From ef18e0e1cd5501b3fad8a7219ad271ca3c1f1a13 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 14 Sep 2026 12:05:52 +0800 Subject: [PATCH 1/3] internal: store document in every node Follow up to https://github.com/lightpanda-io/browser/pull/3501. To recap: many apis use the injected-frame, when, for cross-frame operations, can be wrong. We've been moving to using the Node's Document's Frame on a case- by-case basis. The goal with this series of PRs is to more holistically fix it. The main goal of this commit is to make Node.getDocument fast. Previously, Node.nodeDocument (renamed to Node.getDocument) and Node.ownerFrame had to walk up the _parent tree to find the document. Now there's a DocumentRegistry on the Browser, and every Node has a `_document: u32` index. Thus, Node -> Document is O(1). This makes it so we can have correctness without a performance cost. The DocumentRegistry is held on the Browser because of shared CDP nodes. A follow up should be able to move this to the Page and change the index to u16. Adding a DocumentRegistry to Browser, and a document: u32 to Document is easy. The reason this PR is so big is because every `node_factory` and many Factory methods need to be aware of all of this. You can't create an HTMLDivElement without the document it belongs to. I incorporated more expected future changes into the node_factory/factory mechanism so that [hopefully] the next PRs don't have to touch any of this code (e.g. I removed Frame as a parameter and added a *Page to every Document so that documents have access to factory/arena/ etc..because a `*Document` might not have a `*Frame`, but it will always have a `*Page`. Give every document an index (u32), and store that index in Node. --- src/NodeRegistry.zig | 5 +- src/browser/Browser.zig | 9 + src/browser/DocumentRegistry.zig | 66 +++ src/browser/Factory.zig | 125 ++++-- src/browser/Frame.zig | 17 +- src/browser/Page.zig | 3 +- src/browser/frame/node_factory.zig | 398 ++++++++++--------- src/browser/frame/parse.zig | 17 - src/browser/parser/Parser.zig | 25 +- src/browser/tests/domparser.html | 11 + src/browser/tests/node/owner.html | 60 +++ src/browser/webapi/CData.zig | 6 +- src/browser/webapi/DOMImplementation.zig | 20 +- src/browser/webapi/DOMParser.zig | 9 +- src/browser/webapi/Document.zig | 106 ++--- src/browser/webapi/DocumentFragment.zig | 19 +- src/browser/webapi/DocumentType.zig | 8 +- src/browser/webapi/Element.zig | 22 +- src/browser/webapi/HTMLDocument.zig | 6 +- src/browser/webapi/Node.zig | 114 +++--- src/browser/webapi/Range.zig | 33 +- src/browser/webapi/ShadowRoot.zig | 2 +- src/browser/webapi/cdata/Comment.zig | 2 +- src/browser/webapi/cdata/Text.zig | 4 +- src/browser/webapi/element/Attribute.zig | 16 +- src/browser/webapi/element/Html.zig | 10 +- src/browser/webapi/element/html/Audio.zig | 2 +- src/browser/webapi/element/html/Image.zig | 7 +- src/browser/webapi/element/html/Link.zig | 5 + src/browser/webapi/element/html/Meta.zig | 5 + src/browser/webapi/element/html/Template.zig | 4 +- src/browser/webapi/element/html/TextArea.zig | 2 +- src/browser/webapi/net/XMLHttpRequest.zig | 2 +- src/slab.zig | 14 + 34 files changed, 666 insertions(+), 488 deletions(-) create mode 100644 src/browser/DocumentRegistry.zig diff --git a/src/NodeRegistry.zig b/src/NodeRegistry.zig index 4d7eaeae5..020b46084 100644 --- a/src/NodeRegistry.zig +++ b/src/NodeRegistry.zig @@ -73,8 +73,9 @@ pub fn resetFrame(self: *NodeRegistry, arena: Allocator, frame: *Frame) void { var it = self.lookup_by_id.valueIterator(); while (it.next()) |node_ptr| { const node = node_ptr.*; - const owner = node.dom.ownerFrame(frame) orelse frame; - if (owner._page == page) { + // Nodes live in their page's slab. A sibling page's node must not be + // resolved through this page's frames, so attribute by storage. + if (page.factory.owns(node.dom)) { doomed.append(arena, node) catch return; } } diff --git a/src/browser/Browser.zig b/src/browser/Browser.zig index 519c6a4a0..c626bc132 100644 --- a/src/browser/Browser.zig +++ b/src/browser/Browser.zig @@ -28,6 +28,8 @@ const js = @import("js/js.zig"); const Page = @import("Page.zig"); const Session = @import("Session.zig"); const Viewport = @import("Viewport.zig"); +const DocumentRegistry = @import("DocumentRegistry.zig"); + const Selector = @import("webapi/selector/Selector.zig"); const Geolocation = @import("webapi/geolocation/Geolocation.zig"); const PermissionState = @import("webapi/Permissions.zig").State; @@ -72,6 +74,11 @@ renderer: ?*lp.screenshot.Renderer = null, // Runtime geolocation override geolocation_override: ?Geolocation.Override = null, +// Every Document allocated in this browser session, allows nodes to refer to +// documents by their index. (TODO: this will probably eventually be moved +// to the Page, but we need other changes first) +documents: DocumentRegistry, + // used by sessions to allocate pages. page_pool: std.heap.MemoryPool(Page), @@ -122,6 +129,7 @@ pub fn init(self: *Browser, app: *App, opts: InitOpts) !void { .env = env, .session = null, .page_pool = .empty, + .documents = .init(allocator), .allocator = allocator, .arena_pool = &app.arena_pool, .http_client = undefined, @@ -154,6 +162,7 @@ pub fn deinit(self: *Browser) void { // fire — only now is it safe to free the pool backing their parameters. self.fc_identity_pool.deinit(allocator); self.page_pool.deinit(allocator); + self.documents.deinit(); self.http_client.deinit(); if (self.renderer) |r| r.deinit(); self.clearPermissions(); diff --git a/src/browser/DocumentRegistry.zig b/src/browser/DocumentRegistry.zig new file mode 100644 index 000000000..ea74285ae --- /dev/null +++ b/src/browser/DocumentRegistry.zig @@ -0,0 +1,66 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Every Document created by the Browser. Allows Node to reference them by +// index (u32). + +const std = @import("std"); +const lp = @import("lightpanda"); + +const Document = @import("webapi/Document.zig"); + +const log = lp.log; +const Allocator = std.mem.Allocator; + +const DocumentRegistry = @This(); + +allocator: Allocator, +items: std.ArrayList(*Document) = .empty, +free: std.ArrayList(u32) = .empty, + +pub fn init(allocator: Allocator) DocumentRegistry { + return .{ .allocator = allocator }; +} + +pub fn deinit(self: *DocumentRegistry) void { + self.items.deinit(self.allocator); + self.free.deinit(self.allocator); +} + +pub fn get(self: *const DocumentRegistry, index: u32) *Document { + return self.items.items[index]; +} + +pub fn register(self: *DocumentRegistry, doc: *Document) !u32 { + if (self.free.pop()) |index| { + self.items.items[index] = doc; + return index; + } + const index = std.math.cast(u32, self.items.items.len) orelse { + log.warn(.browser, "document limit", .{ .count = self.items.items.len }); + return error.QuotaExceeded; + }; + try self.items.append(self.allocator, doc); + return index; +} + +pub fn release(self: *DocumentRegistry, index: u32) void { + // The slot keeps its stale pointer; nothing resolves it until reused. + // Failing to record it only loses the slot. + self.free.append(self.allocator, index) catch {}; +} diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index b81f859f5..408d35aa1 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -23,21 +23,24 @@ const reflect = @import("reflect.zig"); const SlabAllocator = @import("../slab.zig").SlabAllocator; +const Page = @import("Page.zig"); const Frame = @import("Frame.zig"); +const DocumentRegistry = @import("DocumentRegistry.zig"); + const Node = @import("webapi/Node.zig"); +const Blob = @import("webapi/Blob.zig"); const Event = @import("webapi/Event.zig"); -const UIEvent = @import("webapi/event/UIEvent.zig"); -const MouseEvent = @import("webapi/event/MouseEvent.zig"); +const DOMRect = @import("webapi/DOMRect.zig"); const Element = @import("webapi/Element.zig"); const Document = @import("webapi/Document.zig"); +const UIEvent = @import("webapi/event/UIEvent.zig"); const EventTarget = @import("webapi/EventTarget.zig"); const AbortSignal = @import("webapi/AbortSignal.zig"); -const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig"); -const IDBRequest = @import("webapi/storage/idb/IDBRequest.zig"); -const Blob = @import("webapi/Blob.zig"); +const MouseEvent = @import("webapi/event/MouseEvent.zig"); const AbstractRange = @import("webapi/AbstractRange.zig"); -const DOMRect = @import("webapi/DOMRect.zig"); const DOMRectReadOnly = @import("webapi/DOMRectReadOnly.zig"); +const IDBRequest = @import("webapi/storage/idb/IDBRequest.zig"); +const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig"); const log = lp.log; const String = lp.String; @@ -47,20 +50,42 @@ const Allocator = std.mem.Allocator; // Shared across all frames of a Page. const Factory = @This(); +_page: *Page, _arena: Allocator, _slab: SlabAllocator, +_documents: std.ArrayList(u32) = .empty, // ids of the documents _we_ created +_document_registry: *DocumentRegistry, // &browser.documents -pub fn init(arena: Allocator) Factory { +pub fn init(page: *Page, arena: Allocator, document_registry: *DocumentRegistry) Factory { return .{ + ._page = page, ._arena = arena, ._slab = SlabAllocator.init(arena, 128), + ._document_registry = document_registry, }; } +pub fn deinit(self: *Factory) void { + for (self._documents.items) |index| { + self._document_registry.release(index); + } +} + pub fn storageAllocator(self: *Factory) Allocator { return self._slab.allocator(); } +fn registerDocument(self: *Factory, doc: *Document) !u32 { + const index = try self._document_registry.register(doc); + try self._documents.append(self._arena, index); + return index; +} + +// Whether `ptr` lives in this page's node storage. +pub fn owns(self: *const Factory, ptr: *const anyopaque) bool { + return self._slab.owns(ptr); +} + // this is a root object pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { return self.eventTargetWithAllocator(self._slab.allocator(), child); @@ -224,6 +249,22 @@ fn AutoPrototypeChain(comptime types: []const type) type { chain.setLeaf(types.len - 1, leaf_value); return chain.get(types.len - 1); } + + // Same, for a node chain: stamps the Node with its document. + fn createOwned(allocator: std.mem.Allocator, owner: u32, leaf_value: anytype) !*@TypeOf(leaf_value) { + comptime assert(types[1] == Node); + const chain = try PrototypeChain(types).allocate(allocator); + + chain.setRoot(); + + inline for (1..types.len - 1) |i| { + chain.setMiddle(i); + } + + chain.setLeaf(types.len - 1, leaf_value); + chain.get(1)._owner = owner; + return chain.get(types.len - 1); + } }; } @@ -303,24 +344,62 @@ pub fn domRect(self: *Factory, rect: DOMRectReadOnly.Data) !*DOMRect { return chain.get(1); } -pub fn node(self: *Factory, child: anytype) !*@TypeOf(child) { +pub fn node(self: *Factory, owner: *const Document, child: anytype) !*@TypeOf(child) { + comptime assert(@TypeOf(child) != Document); const allocator = self._slab.allocator(); return try AutoPrototypeChain( &.{ EventTarget, Node, @TypeOf(child) }, - ).create(allocator, child); + ).createOwned(allocator, owner._index, child); +} + +pub const DocumentOpts = struct { + url: ?[:0]const u8 = null, + charset: ?[]const u8 = null, +}; + +// A Document with no more specific type (`new Document()`, XHR's responseXML). +pub fn genericDocument(self: *Factory, opts: DocumentOpts) !*Document { + const chain = try self.documentChain(&.{ EventTarget, Node, Document }, opts); + return chain.get(2); +} + +// Documents: {EventTarget, Node, Document, [Leaf]}. The Document is registered +// in the browser's table as it is built, so it knows its page and slot from +// the start. +fn documentChain(self: *Factory, comptime types: []const type, opts: DocumentOpts) !PrototypeChain(types) { + comptime assert(types[1] == Node and types[2] == Document); + const chain = try PrototypeChain(types).allocate(self._slab.allocator()); + const doc = chain.get(2); + const index = try self.registerDocument(doc); + + chain.setRoot(); + chain.setMiddle(1); + chain.get(1)._owner = index; + + doc.* = .{ + ._proto = undefined, + ._type = if (comptime types.len == 3) .generic else typeInit(Document, chain.get(3)), + ._page = self._page, + ._index = index, + ._url = opts.url, + ._charset = opts.charset, + }; + setProto(doc, chain.get(1)); + return chain; } // CData nodes: {EventTarget, Node, CData, [Text,] Leaf}. // CData is special, it's _type is a bare tag, not a tagged union. A website can // have tens of thousands of Text nodes, and this allows a few optimization to // both reduce the # of allocations and the size -pub fn cdataNode(self: *Factory, cd: Node.CData, leaf: anytype) !*Node.CData { +pub fn cdataNode(self: *Factory, owner: *const Document, cd: Node.CData, leaf: anytype) !*Node.CData { const types = comptime prototypeTypes(@TypeOf(leaf)); comptime assert(types[0] == EventTarget and types[1] == Node and types[2] == Node.CData); const chain = try PrototypeChain(types).allocate(self._slab.allocator()); chain.setRoot(); chain.setMiddle(1); + chain.get(1)._owner = owner._index; const cd_ptr = chain.get(2); cd_ptr.* = cd; @@ -436,41 +515,40 @@ fn ChainedLeaf(comptime Values: type) type { } pub fn document(self: *Factory, child: anytype) !*@TypeOf(child) { - const allocator = self._slab.allocator(); - return try AutoPrototypeChain( - &.{ EventTarget, Node, Document, @TypeOf(child) }, - ).create(allocator, child); + const chain = try self.documentChain(&.{ EventTarget, Node, Document, @TypeOf(child) }, .{}); + chain.setLeaf(3, child); + return chain.get(3); } -pub fn documentFragment(self: *Factory, child: anytype) !*@TypeOf(child) { +pub fn documentFragment(self: *Factory, owner: *const Document, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); return try AutoPrototypeChain( &.{ EventTarget, Node, Node.DocumentFragment, @TypeOf(child) }, - ).create(allocator, child); + ).createOwned(allocator, owner._index, child); } -pub fn element(self: *Factory, child: anytype) !*@TypeOf(child) { +pub fn element(self: *Factory, owner: *const Document, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); return try AutoPrototypeChain( &.{ EventTarget, Node, Element, @TypeOf(child) }, - ).create(allocator, child); + ).createOwned(allocator, owner._index, child); } -pub fn htmlElement(self: *Factory, child: anytype) !*@TypeOf(child) { +pub fn htmlElement(self: *Factory, owner: *const Document, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); return try AutoPrototypeChain( &.{ EventTarget, Node, Element, Element.Html, @TypeOf(child) }, - ).create(allocator, child); + ).createOwned(allocator, owner._index, child); } -pub fn htmlMediaElement(self: *Factory, child: anytype) !*@TypeOf(child) { +pub fn htmlMediaElement(self: *Factory, owner: *const Document, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); return try AutoPrototypeChain( &.{ EventTarget, Node, Element, Element.Html, Element.Html.Media, @TypeOf(child) }, - ).create(allocator, child); + ).createOwned(allocator, owner._index, child); } -pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeOf(child) { +pub fn svgElement(self: *Factory, owner: *const Document, tag_name: []const u8, child: anytype) !*@TypeOf(child) { const types = comptime svgPrototypeTypes(@TypeOf(child)); const chain = try PrototypeChain(types).allocate(self._slab.allocator()); @@ -489,6 +567,7 @@ pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeO } } chain.setLeaf(types.len - 1, child); + chain.get(1)._owner = owner._index; return chain.get(types.len - 1); } diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 1a00f632e..21a452d83 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -151,7 +151,6 @@ _element_rel_lists: Element.RelListLookup = .empty, _element_part_lists: Element.PartListLookup = .empty, _element_token_lists: Element.TokenListLookup = .empty, _element_shadow_roots: Element.ShadowRootLookup = .empty, -_node_owner_documents: Node.OwnerDocumentLookup = .empty, _element_scroll_positions: Element.ScrollPositionLookup = .empty, _element_namespace_uris: Element.NamespaceUriLookup = .empty, _svg_animated_enumerations: AnimatedEnumeration.Lookup = .empty, @@ -255,9 +254,6 @@ _custom_element_creation: enum { // constructor must not run (you end up in an endless loop if the constructor // does this.innerHTML = '...', which happens). bare_context, - // The target document has no custom element registry (e.g. DOMParser). The - // element stays undefined until it's inserted into the frame's document. - undefined, } = .construct, // List of custom elements that were created before their definition was registered @@ -2598,20 +2594,9 @@ pub fn nodeComplete(self: *Frame, node: *Node) !void { return self.nodeIsReady(true, node); } -// Sets the owner document for a node. Only stores entries for nodes whose owner -// is NOT frame.document to minimize memory overhead. -pub fn setNodeOwnerDocument(self: *Frame, node: *Node, owner: *Document) !void { - if (owner == self.document) { - // No need to store if it's the main document - remove if present - _ = self._node_owner_documents.remove(node); - } else { - try self._node_owner_documents.put(self.arena, node, owner); - } -} - // Recursively sets the owner document for a node and all its descendants pub fn adoptNodeTree(self: *Frame, node: *Node, old_owner: *Document, new_owner: *Document) !void { - try self.setNodeOwnerDocument(node, new_owner); + node._owner = new_owner._index; // Per spec, adopted steps run on each element after its document is set. if (node.is(Element)) |el| { diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 58f30497f..75df14720 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -181,7 +181,7 @@ pub fn init(self: *Page, session: *Session, frame_id: u32) !void { .frame = undefined, ._frame_arena = frame_arena, .frame_arena = frame_arena.allocator(), - .factory = Factory.init(frame_arena.allocator()), + .factory = Factory.init(self, frame_arena.allocator(), &session.browser.documents), .globals = .init(session.browser.app.allocator), }; self.queued_navigation = &self.queued_navigation_1; @@ -254,6 +254,7 @@ pub fn deinit(self: *Page) void { self.origins = .empty; } + self.factory.deinit(); self._frame_arena.release(); } diff --git a/src/browser/frame/node_factory.zig b/src/browser/frame/node_factory.zig index bb0e2b3c5..8ebc709bd 100644 --- a/src/browser/frame/node_factory.zig +++ b/src/browser/frame/node_factory.zig @@ -18,8 +18,7 @@ // Node creation for a frame: the createElementNS comptime tag dispatch and its // element-building helpers, plus the Text/Comment/CDATASection/ProcessingInstruction -// factories and XML Name validation. All allocate through the frame's Factory and -// arenas; these functions operate on a *Frame. +// factories and XML Name validation. Nodes are created for a Document. const std = @import("std"); const lp = @import("lightpanda"); @@ -38,50 +37,51 @@ const log = lp.log; const String = lp.String; const IFrame = Element.Html.IFrame; -pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []const u8, attribute_iterator: anytype) !*Node { +pub fn createElementNS(document: *const Node.Document, namespace: Element.Namespace, name: []const u8, attribute_iterator: anytype) !*Node { const from_parser = @TypeOf(attribute_iterator) == Parser.AttributeIterator; + const frame = frameOf(document); switch (namespace) { .html => { switch (name.len) { 1 => switch (name[0]) { 'p' => return createHtmlElementT( - frame, + document, Element.Html.Paragraph, namespace, attribute_iterator, .{}, ), 'a' => return createHtmlElementT( - frame, + document, Element.Html.Anchor, namespace, attribute_iterator, .{}, ), 'b' => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("b"), ._tag = .b }, ), 'i' => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("i"), ._tag = .i }, ), 'q' => return createHtmlElementT( - frame, + document, Element.Html.Quote, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("q"), ._tag = .quote }, ), 's' => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, @@ -91,126 +91,126 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons }, 2 => switch (@as(u16, @bitCast(name[0..2].*))) { asUint("br") => return createHtmlElementT( - frame, + document, Element.Html.BR, namespace, attribute_iterator, .{}, ), asUint("ol") => return createHtmlElementT( - frame, + document, Element.Html.OL, namespace, attribute_iterator, .{}, ), asUint("ul") => return createHtmlElementT( - frame, + document, Element.Html.UL, namespace, attribute_iterator, .{}, ), asUint("li") => return createHtmlElementT( - frame, + document, Element.Html.LI, namespace, attribute_iterator, .{}, ), asUint("h1") => return createHtmlElementT( - frame, + document, Element.Html.Heading, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("h1"), ._tag = .h1 }, ), asUint("h2") => return createHtmlElementT( - frame, + document, Element.Html.Heading, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("h2"), ._tag = .h2 }, ), asUint("h3") => return createHtmlElementT( - frame, + document, Element.Html.Heading, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("h3"), ._tag = .h3 }, ), asUint("h4") => return createHtmlElementT( - frame, + document, Element.Html.Heading, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("h4"), ._tag = .h4 }, ), asUint("h5") => return createHtmlElementT( - frame, + document, Element.Html.Heading, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("h5"), ._tag = .h5 }, ), asUint("h6") => return createHtmlElementT( - frame, + document, Element.Html.Heading, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("h6"), ._tag = .h6 }, ), asUint("hr") => return createHtmlElementT( - frame, + document, Element.Html.HR, namespace, attribute_iterator, .{}, ), asUint("em") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("em"), ._tag = .em }, ), asUint("dd") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("dd"), ._tag = .dd }, ), asUint("dl") => return createHtmlElementT( - frame, + document, Element.Html.DList, namespace, attribute_iterator, .{}, ), asUint("dt") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("dt"), ._tag = .dt }, ), asUint("td") => return createHtmlElementT( - frame, + document, Element.Html.TableCell, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("td"), ._tag = .td }, ), asUint("th") => return createHtmlElementT( - frame, + document, Element.Html.TableCell, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("th"), ._tag = .th }, ), asUint("tr") => return createHtmlElementT( - frame, + document, Element.Html.TableRow, namespace, attribute_iterator, @@ -220,84 +220,84 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons }, 3 => switch (@as(u24, @bitCast(name[0..3].*))) { asUint("div") => return createHtmlElementT( - frame, + document, Element.Html.Div, namespace, attribute_iterator, .{}, ), asUint("img") => return createHtmlElementT( - frame, + document, Element.Html.Image, namespace, attribute_iterator, .{}, ), asUint("nav") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("nav"), ._tag = .nav }, ), asUint("del") => return createHtmlElementT( - frame, + document, Element.Html.Mod, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("del"), ._tag = .del }, ), asUint("ins") => return createHtmlElementT( - frame, + document, Element.Html.Mod, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("ins"), ._tag = .ins }, ), asUint("col") => return createHtmlElementT( - frame, + document, Element.Html.TableCol, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("col"), ._tag = .col }, ), asUint("dir") => return createHtmlElementT( - frame, + document, Element.Html.Directory, namespace, attribute_iterator, .{}, ), asUint("map") => return createHtmlElementT( - frame, + document, Element.Html.Map, namespace, attribute_iterator, .{}, ), asUint("pre") => return createHtmlElementT( - frame, + document, Element.Html.Pre, namespace, attribute_iterator, .{}, ), asUint("sub") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("sub"), ._tag = .sub }, ), asUint("sup") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("sup"), ._tag = .sup }, ), asUint("dfn") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, @@ -307,35 +307,35 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons }, 4 => switch (@as(u32, @bitCast(name[0..4].*))) { asUint("span") => return createHtmlElementT( - frame, + document, Element.Html.Span, namespace, attribute_iterator, .{}, ), asUint("meta") => return createHtmlElementT( - frame, + document, Element.Html.Meta, namespace, attribute_iterator, .{}, ), asUint("link") => return createHtmlElementT( - frame, + document, Element.Html.Link, namespace, attribute_iterator, .{}, ), asUint("slot") => return createHtmlElementT( - frame, + document, Element.Html.Slot, namespace, attribute_iterator, .{}, ), asUint("html") => return createHtmlElementT( - frame, + document, Element.Html.Html, namespace, attribute_iterator, @@ -343,28 +343,30 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons ), asUint("head") => { // Inject user-provided scripts. - const inject_scripts = frame._session.inject_scripts; - const should_inject_scripts = from_parser and frame._parse_mode == .document and inject_scripts.len > 0; + if (document._frame) |realm| { + const inject_scripts = realm._session.inject_scripts; + const should_inject_scripts = from_parser and realm._parse_mode == .document and inject_scripts.len > 0; - if (should_inject_scripts) { - var ls: JS.Local.Scope = undefined; - frame.js.localScope(&ls); - defer ls.deinit(); + if (should_inject_scripts) { + var ls: JS.Local.Scope = undefined; + realm.js.localScope(&ls); + defer ls.deinit(); - for (inject_scripts) |inject_script| { - var try_catch: JS.TryCatch = undefined; - try_catch.init(&ls.local); - defer try_catch.deinit(); + for (inject_scripts) |inject_script| { + var try_catch: JS.TryCatch = undefined; + try_catch.init(&ls.local); + defer try_catch.deinit(); - ls.local.eval(inject_script, "inject_script") catch |err| { - const caught = try_catch.caughtOrError(frame.local_arena, err); - log.err(.app, "inject script error", .{ .err = caught }); - }; + ls.local.eval(inject_script, "inject_script") catch |err| { + const caught = try_catch.caughtOrError(realm.local_arena, err); + log.err(.app, "inject script error", .{ .err = caught }); + }; + } } } return createHtmlElementT( - frame, + document, Element.Html.Head, namespace, attribute_iterator, @@ -372,28 +374,28 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons ); }, asUint("body") => return createHtmlElementT( - frame, + document, Element.Html.Body, namespace, attribute_iterator, .{}, ), asUint("form") => return createHtmlElementT( - frame, + document, Element.Html.Form, namespace, attribute_iterator, .{}, ), asUint("main") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("main"), ._tag = .main }, ), asUint("data") => return createHtmlElementT( - frame, + document, Element.Html.Data, namespace, attribute_iterator, @@ -401,53 +403,55 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons ), asUint("base") => { const n = try createHtmlElementT( - frame, + document, Element.Html.Base, namespace, attribute_iterator, .{}, ); - // If frames's base url is not already set, fill it with - // the base tag. - if (frame.base_url == null) { - if (n.as(Element).getAttributeInterned("href")) |href| { - frame.base_url = try URL.resolve(frame.arena, frame.url, href, .{}); + // If the frame's base url is not already set, fill it + // with the base tag. + if (document._frame) |realm| { + if (realm.base_url == null) { + if (n.as(Element).getAttributeInterned("href")) |href| { + realm.base_url = try URL.resolve(realm.arena, realm.url, href, .{}); + } } } return n; }, asUint("menu") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("menu"), ._tag = .menu }, ), asUint("area") => return createHtmlElementT( - frame, + document, Element.Html.Area, namespace, attribute_iterator, .{}, ), asUint("font") => return createHtmlElementT( - frame, + document, Element.Html.Font, namespace, attribute_iterator, .{}, ), asUint("code") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("code"), ._tag = .code }, ), asUint("time") => return createHtmlElementT( - frame, + document, Element.Html.Time, namespace, attribute_iterator, @@ -457,103 +461,103 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons }, 5 => switch (@as(u40, @bitCast(name[0..5].*))) { asUint("input") => return createHtmlElementT( - frame, + document, Element.Html.Input, namespace, attribute_iterator, .{}, ), asUint("style") => return createHtmlElementT( - frame, + document, Element.Html.Style, namespace, attribute_iterator, .{}, ), asUint("title") => return createHtmlElementT( - frame, + document, Element.Html.Title, namespace, attribute_iterator, .{}, ), asUint("embed") => return createHtmlElementT( - frame, + document, Element.Html.Embed, namespace, attribute_iterator, .{}, ), asUint("audio") => return createHtmlMediaElementT( - frame, + document, Element.Html.Media.Audio, namespace, attribute_iterator, ), asUint("video") => return createHtmlMediaElementT( - frame, + document, Element.Html.Media.Video, namespace, attribute_iterator, ), asUint("aside") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("aside"), ._tag = .aside }, ), asUint("label") => return createHtmlElementT( - frame, + document, Element.Html.Label, namespace, attribute_iterator, .{}, ), asUint("meter") => return createHtmlElementT( - frame, + document, Element.Html.Meter, namespace, attribute_iterator, .{}, ), asUint("param") => return createHtmlElementT( - frame, + document, Element.Html.Param, namespace, attribute_iterator, .{}, ), asUint("table") => return createHtmlElementT( - frame, + document, Element.Html.Table, namespace, attribute_iterator, .{}, ), asUint("thead") => return createHtmlElementT( - frame, + document, Element.Html.TableSection, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("thead"), ._tag = .thead }, ), asUint("tbody") => return createHtmlElementT( - frame, + document, Element.Html.TableSection, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("tbody"), ._tag = .tbody }, ), asUint("tfoot") => return createHtmlElementT( - frame, + document, Element.Html.TableSection, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("tfoot"), ._tag = .tfoot }, ), asUint("track") => return createHtmlElementT( - frame, + document, Element.Html.Track, namespace, attribute_iterator, @@ -563,112 +567,112 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons }, 6 => switch (@as(u48, @bitCast(name[0..6].*))) { asUint("script") => return createHtmlElementT( - frame, + document, Element.Html.Script, namespace, attribute_iterator, .{}, ), asUint("button") => return createHtmlElementT( - frame, + document, Element.Html.Button, namespace, attribute_iterator, .{}, ), asUint("canvas") => return createHtmlElementT( - frame, + document, Element.Html.Canvas, namespace, attribute_iterator, .{}, ), asUint("dialog") => return createHtmlElementT( - frame, + document, Element.Html.Dialog, namespace, attribute_iterator, .{}, ), asUint("legend") => return createHtmlElementT( - frame, + document, Element.Html.Legend, namespace, attribute_iterator, .{}, ), asUint("object") => return createHtmlElementT( - frame, + document, Element.Html.Object, namespace, attribute_iterator, .{}, ), asUint("output") => return createHtmlElementT( - frame, + document, Element.Html.Output, namespace, attribute_iterator, .{}, ), asUint("source") => return createHtmlElementT( - frame, + document, Element.Html.Source, namespace, attribute_iterator, .{}, ), asUint("strong") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("strong"), ._tag = .strong }, ), asUint("header") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("header"), ._tag = .header }, ), asUint("footer") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("footer"), ._tag = .footer }, ), asUint("select") => return createHtmlElementT( - frame, + document, Element.Html.Select, namespace, attribute_iterator, .{}, ), asUint("option") => return createHtmlElementT( - frame, + document, Element.Html.Option, namespace, attribute_iterator, .{}, ), asUint("iframe") => return createHtmlElementT( - frame, + document, IFrame, namespace, attribute_iterator, .{}, ), asUint("figure") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("figure"), ._tag = .figure }, ), asUint("hgroup") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, @@ -678,56 +682,56 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons }, 7 => switch (@as(u56, @bitCast(name[0..7].*))) { asUint("section") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("section"), ._tag = .section }, ), asUint("article") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("article"), ._tag = .article }, ), asUint("details") => return createHtmlElementT( - frame, + document, Element.Html.Details, namespace, attribute_iterator, .{}, ), asUint("summary") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("summary"), ._tag = .summary }, ), asUint("caption") => return createHtmlElementT( - frame, + document, Element.Html.TableCaption, namespace, attribute_iterator, .{}, ), asUint("marquee") => return createHtmlElementT( - frame, + document, Element.Html.Marquee, namespace, attribute_iterator, .{}, ), asUint("address") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("address"), ._tag = .address }, ), asUint("picture") => return createHtmlElementT( - frame, + document, Element.Html.Picture, namespace, attribute_iterator, @@ -737,28 +741,28 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons }, 8 => switch (@as(u64, @bitCast(name[0..8].*))) { asUint("textarea") => return createHtmlElementT( - frame, + document, Element.Html.TextArea, namespace, attribute_iterator, .{}, ), asUint("template") => return createHtmlElementT( - frame, + document, Element.Html.Template, namespace, attribute_iterator, .{ ._content = undefined }, ), asUint("colgroup") => return createHtmlElementT( - frame, + document, Element.Html.TableCol, namespace, attribute_iterator, .{ ._tag_name = comptime .wrap("colgroup"), ._tag = .colgroup }, ), asUint("fieldset") => return createHtmlElementT( - frame, + document, Element.Html.FieldSet, namespace, attribute_iterator, @@ -769,7 +773,7 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons log.warn(.not_implemented, "framset", .{ .note = "... in html is not handled properly" }); } return createHtmlElementT( - frame, + document, Element.Html.FrameSet, namespace, attribute_iterator, @@ -777,28 +781,28 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons ); }, asUint("optgroup") => return createHtmlElementT( - frame, + document, Element.Html.OptGroup, namespace, attribute_iterator, .{}, ), asUint("progress") => return createHtmlElementT( - frame, + document, Element.Html.Progress, namespace, attribute_iterator, .{}, ), asUint("datalist") => return createHtmlElementT( - frame, + document, Element.Html.DataList, namespace, attribute_iterator, .{}, ), asUint("noscript") => return createHtmlElementT( - frame, + document, Element.Html.Generic, namespace, attribute_iterator, @@ -808,7 +812,7 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons }, 10 => switch (@as(u80, @bitCast(name[0..10].*))) { asUint("blockquote") => return createHtmlElementT( - frame, + document, Element.Html.Quote, namespace, attribute_iterator, @@ -823,44 +827,46 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons // Check if this is a custom element (must have hyphen for HTML namespace) const has_hyphen = std.mem.indexOfScalar(u8, name, '-') != null; if (has_hyphen and namespace == .html) { - const creation = frame._custom_element_creation; - const definition = switch (creation) { - .construct, .bare_context => frame.window._custom_elements._definitions.get(name), - // A windowless document has no registry. - .undefined => null, + // A document without a browsing context has no registry: its + // elements stay undefined until inserted into a document that + // has one. + const realm = document._frame orelse { + return createHtmlElementT(document, Element.Html.Custom, namespace, attribute_iterator, .{ + ._tag_name = tag_name, + ._definition = null, + }); }; + const creation = realm._custom_element_creation; + const definition = realm.window._custom_elements._definitions.get(name); // Fragment-parse context element. It will not be inserted and // we should not run the custom element's constructor. // // Undefined elements are created in the "undefined" state and // upgraded later, when a matching definition is registered. - // Only elements created for this frame's document are - // candidates: a windowless document's element is upgraded on - // insertion into this frame's document instead. if (creation != .construct or definition == null) { - const node = try createHtmlElementT(frame, Element.Html.Custom, namespace, attribute_iterator, .{ + const node = try createHtmlElementT(document, Element.Html.Custom, namespace, attribute_iterator, .{ ._tag_name = tag_name, ._definition = definition, }); if (creation == .construct) { - try frame._undefined_custom_elements.append(frame.arena, node.as(Element).is(Element.Html.Custom).?); + try realm._undefined_custom_elements.append(realm.arena, node.as(Element).is(Element.Html.Custom).?); } return node; } // https://dom.spec.whatwg.org/#concept-create-element, the // synchronous branch. super() has to create its own element - const constructed = constructForToken(frame, definition.?, tag_name, from_parser) catch { + const constructed = constructForToken(realm, definition.?, tag_name, from_parser) catch { // Construction failed, we fallback to HTMLUnknownElement - return createHtmlElementT(frame, Element.Html.Unknown, namespace, attribute_iterator, .{ + return createHtmlElementT(document, Element.Html.Unknown, namespace, attribute_iterator, .{ ._tag_name = tag_name, }); }; // Attributes are applied after construction, so the constructor // observes none of them and each one enqueues its reaction. - try populateElementAttributes(frame, constructed, attribute_iterator); + try populateElementAttributes(realm, constructed, attribute_iterator); for (constructed.attributeEntries()) |*attr| { Element.Html.Custom.enqueueAttributeChangedCallbackOnElement( constructed, @@ -868,14 +874,14 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons null, // old_value is null for initial attributes .wrap(attr.value()), null, - frame, + realm, ); } return constructed.asNode(); } - return createHtmlElementT(frame, Element.Html.Unknown, namespace, attribute_iterator, .{ ._tag_name = tag_name }); + return createHtmlElementT(document, Element.Html.Unknown, namespace, attribute_iterator, .{ ._tag_name = tag_name }); }, .svg => { const Graphics = Element.Svg.Graphics; @@ -883,60 +889,60 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons // SVG tag names are case-sensitive; no lowering before matching. switch (name.len) { 1 => switch (name[0]) { - 'g' => return createSvgElementT(frame, Graphics.G, name, attribute_iterator, .{}), - 'a' => return createSvgElementT(frame, Graphics.A, name, attribute_iterator, .{}), + 'g' => return createSvgElementT(document, Graphics.G, name, attribute_iterator, .{}), + 'a' => return createSvgElementT(document, Graphics.A, name, attribute_iterator, .{}), else => {}, }, 3 => switch (@as(u24, @bitCast(name[0..3].*))) { - asUint("svg") => return createSvgElementT(frame, Graphics.Svg, name, attribute_iterator, .{}), - asUint("use") => return createSvgElementT(frame, Graphics.Use, name, attribute_iterator, .{}), + asUint("svg") => return createSvgElementT(document, Graphics.Svg, name, attribute_iterator, .{}), + asUint("use") => return createSvgElementT(document, Graphics.Use, name, attribute_iterator, .{}), else => {}, }, 4 => switch (@as(u32, @bitCast(name[0..4].*))) { - asUint("defs") => return createSvgElementT(frame, Graphics.Defs, name, attribute_iterator, .{}), - asUint("desc") => return createSvgElementT(frame, Element.Svg.Desc, name, attribute_iterator, .{}), - asUint("mask") => return createSvgElementT(frame, Element.Svg.Mask, name, attribute_iterator, .{}), - asUint("rect") => return createSvgElementT(frame, Geometry.Rect, name, attribute_iterator, .{}), - asUint("stop") => return createSvgElementT(frame, Element.Svg.Stop, name, attribute_iterator, .{}), - asUint("text") => return createSvgElementT(frame, Graphics.TextContent.TextPositioning.Text, name, attribute_iterator, .{}), - asUint("line") => return createSvgElementT(frame, Geometry.Line, name, attribute_iterator, .{}), - asUint("path") => return createSvgElementT(frame, Geometry.Path, name, attribute_iterator, .{}), - asUint("view") => return createSvgElementT(frame, Element.Svg.View, name, attribute_iterator, .{}), + asUint("defs") => return createSvgElementT(document, Graphics.Defs, name, attribute_iterator, .{}), + asUint("desc") => return createSvgElementT(document, Element.Svg.Desc, name, attribute_iterator, .{}), + asUint("mask") => return createSvgElementT(document, Element.Svg.Mask, name, attribute_iterator, .{}), + asUint("rect") => return createSvgElementT(document, Geometry.Rect, name, attribute_iterator, .{}), + asUint("stop") => return createSvgElementT(document, Element.Svg.Stop, name, attribute_iterator, .{}), + asUint("text") => return createSvgElementT(document, Graphics.TextContent.TextPositioning.Text, name, attribute_iterator, .{}), + asUint("line") => return createSvgElementT(document, Geometry.Line, name, attribute_iterator, .{}), + asUint("path") => return createSvgElementT(document, Geometry.Path, name, attribute_iterator, .{}), + asUint("view") => return createSvgElementT(document, Element.Svg.View, name, attribute_iterator, .{}), else => {}, }, 5 => switch (@as(u40, @bitCast(name[0..5].*))) { - asUint("image") => return createSvgElementT(frame, Graphics.Image, name, attribute_iterator, .{}), - asUint("title") => return createSvgElementT(frame, Element.Svg.Title, name, attribute_iterator, .{}), - asUint("tspan") => return createSvgElementT(frame, Graphics.TextContent.TextPositioning.TSpan, name, attribute_iterator, .{}), + asUint("image") => return createSvgElementT(document, Graphics.Image, name, attribute_iterator, .{}), + asUint("title") => return createSvgElementT(document, Element.Svg.Title, name, attribute_iterator, .{}), + asUint("tspan") => return createSvgElementT(document, Graphics.TextContent.TextPositioning.TSpan, name, attribute_iterator, .{}), else => {}, }, 6 => switch (@as(u48, @bitCast(name[0..6].*))) { - asUint("circle") => return createSvgElementT(frame, Geometry.Circle, name, attribute_iterator, .{}), - asUint("marker") => return createSvgElementT(frame, Element.Svg.Marker, name, attribute_iterator, .{}), - asUint("switch") => return createSvgElementT(frame, Graphics.Switch, name, attribute_iterator, .{}), - asUint("symbol") => return createSvgElementT(frame, Graphics.Symbol, name, attribute_iterator, .{}), + asUint("circle") => return createSvgElementT(document, Geometry.Circle, name, attribute_iterator, .{}), + asUint("marker") => return createSvgElementT(document, Element.Svg.Marker, name, attribute_iterator, .{}), + asUint("switch") => return createSvgElementT(document, Graphics.Switch, name, attribute_iterator, .{}), + asUint("symbol") => return createSvgElementT(document, Graphics.Symbol, name, attribute_iterator, .{}), else => {}, }, 7 => switch (@as(u56, @bitCast(name[0..7].*))) { - asUint("ellipse") => return createSvgElementT(frame, Geometry.Ellipse, name, attribute_iterator, .{}), - asUint("pattern") => return createSvgElementT(frame, Element.Svg.Pattern, name, attribute_iterator, .{}), - asUint("polygon") => return createSvgElementT(frame, Geometry.Polygon, name, attribute_iterator, .{}), + asUint("ellipse") => return createSvgElementT(document, Geometry.Ellipse, name, attribute_iterator, .{}), + asUint("pattern") => return createSvgElementT(document, Element.Svg.Pattern, name, attribute_iterator, .{}), + asUint("polygon") => return createSvgElementT(document, Geometry.Polygon, name, attribute_iterator, .{}), else => {}, }, 8 => switch (@as(u64, @bitCast(name[0..8].*))) { - asUint("clipPath") => return createSvgElementT(frame, Element.Svg.ClipPath, name, attribute_iterator, .{}), - asUint("metadata") => return createSvgElementT(frame, Element.Svg.Metadata, name, attribute_iterator, .{}), - asUint("polyline") => return createSvgElementT(frame, Geometry.Polyline, name, attribute_iterator, .{}), - asUint("textPath") => return createSvgElementT(frame, Graphics.TextContent.TextPath, name, attribute_iterator, .{}), + asUint("clipPath") => return createSvgElementT(document, Element.Svg.ClipPath, name, attribute_iterator, .{}), + asUint("metadata") => return createSvgElementT(document, Element.Svg.Metadata, name, attribute_iterator, .{}), + asUint("polyline") => return createSvgElementT(document, Geometry.Polyline, name, attribute_iterator, .{}), + asUint("textPath") => return createSvgElementT(document, Graphics.TextContent.TextPath, name, attribute_iterator, .{}), else => {}, }, 13 => switch (@as(u104, @bitCast(name[0..13].*))) { - asUint("foreignObject") => return createSvgElementT(frame, Graphics.ForeignObject, name, attribute_iterator, .{}), + asUint("foreignObject") => return createSvgElementT(document, Graphics.ForeignObject, name, attribute_iterator, .{}), else => {}, }, 14 => switch (@as(u112, @bitCast(name[0..14].*))) { - asUint("linearGradient") => return createSvgElementT(frame, Element.Svg.GradientElement.LinearGradient, name, attribute_iterator, .{}), - asUint("radialGradient") => return createSvgElementT(frame, Element.Svg.GradientElement.RadialGradient, name, attribute_iterator, .{}), + asUint("linearGradient") => return createSvgElementT(document, Element.Svg.GradientElement.LinearGradient, name, attribute_iterator, .{}), + asUint("radialGradient") => return createSvgElementT(document, Element.Svg.GradientElement.RadialGradient, name, attribute_iterator, .{}), else => {}, }, else => {}, @@ -944,11 +950,11 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons const lower = std.ascii.lowerString(&frame.buf, name); const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown; - return createSvgElementT(frame, Element.Svg.Generic, name, attribute_iterator, .{ ._tag = tag }); + return createSvgElementT(document, Element.Svg.Generic, name, attribute_iterator, .{ ._tag = tag }); }, else => { const tag_name = try String.init(frame.arena, name, .{}); - return createHtmlElementT(frame, Element.Html.Unknown, namespace, attribute_iterator, .{ ._tag_name = tag_name }); + return createHtmlElementT(document, Element.Html.Unknown, namespace, attribute_iterator, .{ ._tag_name = tag_name }); }, } } @@ -1038,15 +1044,19 @@ fn notSupportedError(local: *const JS.Local) JS.Value { return local.zigValueToJs(ex, .{}) catch .{ .local = local, .handle = local.isolate.createError("not supported") }; } -fn createHtmlElementT(frame: *Frame, comptime E: type, namespace: Element.Namespace, attribute_iterator: anytype, html_element: E) !*Node { - const html_element_ptr = try frame._factory.htmlElement(html_element); +fn createHtmlElementT(document: *const Node.Document, comptime E: type, namespace: Element.Namespace, attribute_iterator: anytype, html_element: E) !*Node { + const frame = frameOf(document); + const html_element_ptr = try frame._factory.htmlElement(document, html_element); const element = html_element_ptr.asElement(); element._namespace = namespace; element._attributes.normalize = namespace == .html; try populateElementAttributes(frame, element, attribute_iterator); - // Check for customized built-in element via "is" attribute - try Element.Html.Custom.checkAndAttachBuiltIn(element, frame); + // Check for customized built-in element via "is" attribute. A document + // without a browsing context has no registry. + if (document._frame != null) { + try Element.Html.Custom.checkAndAttachBuiltIn(element, frame); + } const node = element.asNode(); if (@hasDecl(E, "Build") and @hasDecl(E.Build, "created")) { @@ -1058,16 +1068,18 @@ fn createHtmlElementT(frame: *Frame, comptime E: type, namespace: Element.Namesp return node; } -fn createHtmlMediaElementT(frame: *Frame, comptime E: type, namespace: Element.Namespace, attribute_iterator: anytype) !*Node { - const media_element = try frame._factory.htmlMediaElement(E{}); +fn createHtmlMediaElementT(document: *const Node.Document, comptime E: type, namespace: Element.Namespace, attribute_iterator: anytype) !*Node { + const frame = frameOf(document); + const media_element = try frame._factory.htmlMediaElement(document, E{}); const element = media_element.asElement(); element._namespace = namespace; try populateElementAttributes(frame, element, attribute_iterator); return element.asNode(); } -fn createSvgElementT(frame: *Frame, comptime E: type, tag_name: []const u8, attribute_iterator: anytype, svg_element: E) !*Node { - const svg_element_ptr = try frame._factory.svgElement(tag_name, svg_element); +fn createSvgElementT(document: *const Node.Document, comptime E: type, tag_name: []const u8, attribute_iterator: anytype, svg_element: E) !*Node { + const frame = frameOf(document); + const svg_element_ptr = try frame._factory.svgElement(document, tag_name, svg_element); return initSvgElement(frame, svg_element_ptr.asElement(), attribute_iterator); } @@ -1078,6 +1090,14 @@ fn initSvgElement(frame: *Frame, element: *Element, attribute_iterator: anytype) return element.asNode(); } +// Allocation and scratch for nodes of `document`: its frame's, or the page's +// root frame's for a document without a browsing context. Anything that +// depends on the browsing context itself goes through `document._frame` and is +// skipped when that is null. +fn frameOf(document: *const Node.Document) *Frame { + return document._frame orelse &document._page.frame; +} + fn populateElementAttributes(frame: *Frame, element: *Element, list: anytype) !void { if (@TypeOf(list) == *Element.Attribute.List or @TypeOf(list) == *const Element.Attribute.List) { // from cloneNode @@ -1138,43 +1158,46 @@ pub fn constructCustomElement(frame: *Frame, new_target: JS.Function) !*Element } const tag_name = try String.init(frame.arena, definition.name, .{}); - const node = try createHtmlElementT(frame, Element.Html.Custom, .html, null, .{ + const node = try createHtmlElementT(frame.document, Element.Html.Custom, .html, null, .{ ._tag_name = tag_name, ._definition = definition, }); return node.as(Element); } -pub fn createTextNode(frame: *Frame, text: []const u8) !*Node { - const cd = try frame._factory.cdataNode(.{ +pub fn createTextNode(document: *const Node.Document, text: []const u8) !*Node { + const frame = frameOf(document); + const cd = try frame._factory.cdataNode(document, .{ ._type = .text, ._data = try frame.dupeSSO(text), }, CData.Text{}); return cd.asNode(); } -pub fn createComment(frame: *Frame, text: []const u8) !*Node { - const cd = try frame._factory.cdataNode(.{ +pub fn createComment(document: *const Node.Document, text: []const u8) !*Node { + const frame = frameOf(document); + const cd = try frame._factory.cdataNode(document, .{ ._type = .comment, ._data = try frame.dupeSSO(text), }, CData.Comment{}); return cd.asNode(); } -pub fn createCDATASection(frame: *Frame, data: []const u8) !*Node { +pub fn createCDATASection(document: *const Node.Document, data: []const u8) !*Node { // Validate that the data doesn't contain "]]>" if (std.mem.indexOf(u8, data, "]]>") != null) { return error.InvalidCharacterError; } - const cd = try frame._factory.cdataNode(.{ + const frame = frameOf(document); + const cd = try frame._factory.cdataNode(document, .{ ._type = .cdata_section, ._data = try frame.dupeSSO(data), }, CData.CDATASection{}); return cd.asNode(); } -pub fn createProcessingInstruction(frame: *Frame, target: []const u8, data: []const u8) !*Node { +pub fn createProcessingInstruction(document: *const Node.Document, target: []const u8, data: []const u8) !*Node { // Validate neither target nor data contain "?>" if (std.mem.indexOf(u8, target, "?>") != null) { return error.InvalidCharacterError; @@ -1186,9 +1209,10 @@ pub fn createProcessingInstruction(frame: *Frame, target: []const u8, data: []co // Validate target follows XML Name production try validateXmlName(target); + const frame = frameOf(document); const owned_target = try frame.dupeString(target); - const cd = try frame._factory.cdataNode(.{ + const cd = try frame._factory.cdataNode(document, .{ ._type = .processing_instruction, ._data = try frame.dupeSSO(data), }, CData.ProcessingInstruction{ diff --git a/src/browser/frame/parse.zig b/src/browser/frame/parse.zig index ae7935917..eeadd3f69 100644 --- a/src/browser/frame/parse.zig +++ b/src/browser/frame/parse.zig @@ -46,18 +46,6 @@ pub fn fragment(frame: *Frame, node: *Node, html: []const u8, opts: FragmentPars frame._parse_mode = .fragment; defer frame._parse_mode = previous_parse_mode; - // A context element in a document without a browsing context - // (createHTMLDocument, new Document, DOMParser output) has no custom - // element registry. - const previous_creation = frame._custom_element_creation; - const document = node.ownerDocument(frame) orelse node.as(Document); - if (document._frame == null) { - // a document without a browsing context (DOMParser et al.) has - // no custom element and should stay undefined. - frame._custom_element_creation = .undefined; - } - defer frame._custom_element_creation = previous_creation; - // The html5ever wrapper-unwrap below rebinds children without going // through the insertion path, so recompute slot assignments for any // shadow tree this fragment landed in (idempotent; signals only on diff). @@ -120,11 +108,6 @@ pub fn xmlDocument(frame: *Frame, xml: []const u8) !?*Document.XMLDocument { frame._parse_mode = .fragment; defer frame._parse_mode = previous_parse_mode; - // No browsing context, so no custom element registry. - const previous_creation = frame._custom_element_creation; - frame._custom_element_creation = .undefined; - defer frame._custom_element_creation = previous_creation; - const doc = try frame._factory.document(Document.XMLDocument{ ._proto = undefined }); const doc_node = doc.asNode(); var parser = Parser.init(arena.allocator(), doc_node, frame, .{}); diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index d1c87e9b5..67228d221 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -57,8 +57,9 @@ const Parser = @This(); frame: *Frame, err: ?Error, -container: ParsedNode, arena: Allocator, +container: ParsedNode, +document: *Node.Document, strings: std.StringHashMapUnmanaged(void), pending_text: ?PendingText, // One buffer reused across every text run in this parser. clearRetainingCapacity @@ -96,6 +97,7 @@ pub fn init(arena: Allocator, node: *Node, frame: *Frame, opts: Options) Parser return .{ .err = null, .frame = frame, + .document = node.getDocument(frame), .strings = .empty, .arena = arena, .container = ParsedNode{ @@ -155,7 +157,7 @@ fn appendTextChunk(self: *Parser, parent: *Node, txt: []const u8) !void { // Fresh text run: the first chunk lives on _data only. buf stays empty // until (and unless) a second chunk arrives. - const new_text = try Frame.node_factory.createTextNode(self.frame, txt); + const new_text = try Frame.node_factory.createTextNode(self.document, txt); try self.frame.appendNew(parent, new_text); self.pending_text = .{ .parent = parent, @@ -493,9 +495,10 @@ fn createXMLElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualNa // create. fn createContextElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque { const self: *Parser = @ptrCast(@alignCast(ctx)); - const previous_creation = self.frame._custom_element_creation; - self.frame._custom_element_creation = .bare_context; - defer self.frame._custom_element_creation = previous_creation; + const frame = self.document._frame orelse self.frame; + const previous_creation = frame._custom_element_creation; + frame._custom_element_creation = .bare_context; + defer frame._custom_element_creation = previous_creation; return self._createElementCallback(data, qname, attributes, .unknown) catch |err| { self.err = .{ .err = err, .source = .create_element }; return null; @@ -522,7 +525,7 @@ fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, } else local; const namespace_string = qname.ns.slice(); const namespace = if (namespace_string.len == 0) default_namespace else Element.Namespace.parse(namespace_string); - const node = try Frame.node_factory.createElementNS(frame, namespace, name, attributes); + const node = try Frame.node_factory.createElementNS(self.document, namespace, name, attributes); if (namespace == .unknown and namespace_string.len > 0) { // Same as Document.createElementNS: keep the URI so namespaceURI and // lookupNamespaceURI can return it. @@ -547,8 +550,7 @@ fn createCommentCallback(ctx: *anyopaque, str: h5e.StringSlice) callconv(.c) ?*a }; } fn _createCommentCallback(self: *Parser, str: []const u8) !*anyopaque { - const frame = self.frame; - const node = try Frame.node_factory.createComment(frame, str); + const node = try Frame.node_factory.createComment(self.document, str); const pn = try self.arena.create(ParsedNode); pn.* = .{ .data = null, @@ -567,8 +569,7 @@ fn createProcessingInstruction(ctx: *anyopaque, target: h5e.StringSlice, data: h }; } fn _createProcessingInstruction(self: *Parser, target: []const u8, data: []const u8) !*anyopaque { - const frame = self.frame; - const node = try Frame.node_factory.createProcessingInstruction(frame, target, data); + const node = try Frame.node_factory.createProcessingInstruction(self.document, target, data); const pn = try self.arena.create(ParsedNode); pn.* = .{ .data = null, @@ -593,7 +594,7 @@ fn _appendDoctypeToDocument(self: *Parser, name: []const u8, public_id: []const // Create the DocumentType node const DocumentType = @import("../webapi/DocumentType.zig"); - const doctype = try frame._factory.node(DocumentType{ + const doctype = try frame._factory.node(self.document, DocumentType{ ._proto = undefined, ._name = try frame.dupeString(name), ._public_id = try frame.dupeString(public_id), @@ -801,7 +802,7 @@ fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e } break :blk child; }, - .text => |txt| try Frame.node_factory.createTextNode(self.frame, txt), + .text => |txt| try Frame.node_factory.createTextNode(self.document, txt), .failed => return, }; try self.frame.insertNodeRelative(parent, node, .{ .before = sibling }, .{}); diff --git a/src/browser/tests/domparser.html b/src/browser/tests/domparser.html index 9ecec562a..759f55f30 100644 --- a/src/browser/tests/domparser.html +++ b/src/browser/tests/domparser.html @@ -551,6 +551,17 @@ } + + + + + + diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig index f6be1f2ee..35fc06ac6 100644 --- a/src/browser/webapi/CData.zig +++ b/src/browser/webapi/CData.zig @@ -444,7 +444,7 @@ pub fn before(self: *CData, nodes: []const Node.NodeOrText, frame: *Frame) !void const parent = node.parentNode() orelse return; for (nodes) |node_or_text| { - const child = try node_or_text.toNode(frame); + const child = try node_or_text.toNode(node.getDocument(frame)); _ = try parent.insertBefore(child, node, frame); } } @@ -455,7 +455,7 @@ pub fn after(self: *CData, nodes: []const Node.NodeOrText, frame: *Frame) !void const viable_next = Node.NodeOrText.viableNextSibling(node, nodes); for (nodes) |node_or_text| { - const child = try node_or_text.toNode(frame); + const child = try node_or_text.toNode(node.getDocument(frame)); _ = try parent.insertBefore(child, viable_next, frame); } } @@ -466,7 +466,7 @@ pub fn replaceWith(self: *CData, nodes: []const Node.NodeOrText, frame: *Frame) var rm_ref_node = true; for (nodes) |node_or_text| { - const child = try node_or_text.toNode(frame); + const child = try node_or_text.toNode(ref_node.getDocument(frame)); if (child == ref_node) { rm_ref_node = false; continue; diff --git a/src/browser/webapi/DOMImplementation.zig b/src/browser/webapi/DOMImplementation.zig index 62bce74e9..f489f62c6 100644 --- a/src/browser/webapi/DOMImplementation.zig +++ b/src/browser/webapi/DOMImplementation.zig @@ -39,11 +39,7 @@ fn createDocumentType(self: *const DOMImplementation, qualified_name: []const u8 } } - 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; + return DocumentType.init(self._document, qualified_name, public_id, system_id, frame); } fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString, frame: *Frame) !*Document { @@ -53,7 +49,7 @@ fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString, fr document._charset = "UTF-8"; { - const doctype = try frame._factory.node(DocumentType{ + const doctype = try frame._factory.node(document, DocumentType{ ._proto = undefined, ._name = "html", ._public_id = "", @@ -62,20 +58,20 @@ fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString, fr _ = try document.asNode().appendChild(doctype.asNode(), frame); } - const html_node = try Frame.node_factory.createElementNS(frame, .html, "html", null); + const html_node = try Frame.node_factory.createElementNS(document, .html, "html", null); _ = try document.asNode().appendChild(html_node, frame); - const head_node = try Frame.node_factory.createElementNS(frame, .html, "head", null); + const head_node = try Frame.node_factory.createElementNS(document, .html, "head", null); _ = try html_node.appendChild(head_node, frame); if (title) |t| { - const title_node = try Frame.node_factory.createElementNS(frame, .html, "title", null); + const title_node = try Frame.node_factory.createElementNS(document, .html, "title", null); _ = try head_node.appendChild(title_node, frame); - const text_node = try Frame.node_factory.createTextNode(frame, t.value); + const text_node = try Frame.node_factory.createTextNode(document, t.value); _ = try title_node.appendChild(text_node, frame); } - const body_node = try Frame.node_factory.createElementNS(frame, .html, "body", null); + const body_node = try Frame.node_factory.createElementNS(document, .html, "body", null); _ = try html_node.appendChild(body_node, frame); return document; @@ -119,7 +115,7 @@ fn createDocument(_: *const DOMImplementation, namespace_nullable: js.Nullable([ // Create and append root element if qualified_name provided if (qname.len > 0) { const namespace = Node.Element.Namespace.parse(namespace_); - const root = try Frame.node_factory.createElementNS(frame, namespace, qname, null); + const root = try Frame.node_factory.createElementNS(document, namespace, qname, null); // Store the original URI for unknown namespaces so namespaceURI and // lookupNamespaceURI can return it (mirrors Document.createElementNS). diff --git a/src/browser/webapi/DOMParser.zig b/src/browser/webapi/DOMParser.zig index 0e62b30a3..d75d81429 100644 --- a/src/browser/webapi/DOMParser.zig +++ b/src/browser/webapi/DOMParser.zig @@ -59,11 +59,6 @@ pub fn parseFromString( frame._parse_mode = .fragment; defer frame._parse_mode = previous_parse_mode; - // No browsing context, so no custom element registry. - const previous_creation = frame._custom_element_creation; - frame._custom_element_creation = .undefined; - defer frame._custom_element_creation = previous_creation; - // Create a new HTMLDocument const doc = try frame._factory.document(HTMLDocument{ ._proto = undefined, @@ -112,9 +107,9 @@ const parsererror_ns = "http://www.mozilla.org/newlayout/xml/parsererror.xml"; // in the Mozilla error namespace. fn parserErrorDocument(frame: *Frame) !*Document.XMLDocument { const doc = try frame._factory.document(Document.XMLDocument{ ._proto = undefined }); - const root = try Frame.node_factory.createElementNS(frame, .unknown, "parsererror", null); + const root = try Frame.node_factory.createElementNS(doc.asDocument(), .unknown, "parsererror", null); try frame._element_namespace_uris.put(frame.arena, root.as(Node.Element), parsererror_ns); - const text = try Frame.node_factory.createTextNode(frame, "error"); + const text = try Frame.node_factory.createTextNode(doc.asDocument(), "error"); _ = try root.appendChild(text, frame); _ = try doc.asNode().appendChild(root, frame); return doc; diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 38260a673..05f3bf51c 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -24,6 +24,7 @@ const public_suffix_list = @import("../../data/public_suffix_list.zig"); const URL = @import("../URL.zig"); const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); const Frame = @import("../Frame.zig"); const Parser = @import("../parser/Parser.zig"); @@ -54,6 +55,8 @@ pub const Proto = Node; _type: Type, _proto: *Node, +_page: *Page, +_index: u32, // browser.documents index _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 @@ -373,7 +376,7 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement }; // HTML documents are case-insensitive - lowercase the tag name - const node = try self.createElementNode(ns, normalized_name, frame); + const node = try self.createElementNode(ns, normalized_name); const element = node.as(Element); const options = options_ orelse return element; @@ -389,7 +392,7 @@ pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8 _ = try validateAndExtract(namespace, name, .element); const ns = Element.Namespace.parse(namespace); // Per spec, createElementNS does NOT lowercase (unlike createElement). - const node = try self.createElementNode(ns, name, frame); + const node = try self.createElementNode(ns, name); // Store original URI for unknown namespaces so lookupNamespaceURI can return it if (ns == .unknown) { @@ -401,40 +404,26 @@ pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8 return node.as(Element); } -fn createElementNode(self: *Document, ns: Element.Namespace, name: []const u8, frame: *Frame) !*Node { - const previous_creation = frame._custom_element_creation; - if (self._frame == null) { - // a document without a browser context, e.g. DOMParser, has no custom - // element registry - frame._custom_element_creation = .undefined; - } - defer frame._custom_element_creation = previous_creation; - - const node = try Frame.node_factory.createElementNS(frame, ns, name, null); - - // Track owner document if it's not the main document - if (self != frame.document) { - try frame.setNodeOwnerDocument(node, self); - } - return node; +fn createElementNode(self: *Document, ns: Element.Namespace, name: []const u8) !*Node { + return Frame.node_factory.createElementNS(self, ns, name, null); } -fn createAttribute(_: *const Document, name: String.Global, frame: *Frame) !?*Element.Attribute { +fn createAttribute(self: *const Document, name: String.Global, frame: *Frame) !?*Element.Attribute { try Element.Attribute.validateAttributeName(name.str); - return frame._factory.node(Element.Attribute{ + return frame._factory.node(self, Element.Attribute{ ._name = name.str, ._value = String.empty, ._element = null, }); } -pub fn createAttributeNS(_: *const Document, namespace: []const u8, name: String.Global, frame: *Frame) !?*Element.Attribute { +pub fn createAttributeNS(self: *const Document, namespace: []const u8, name: String.Global, frame: *Frame) !?*Element.Attribute { if (std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml") == false) { log.warn(.not_implemented, "document.createAttributeNS", .{ .namespace = namespace }); } try Element.Attribute.validateAttributeName(name.str); - return frame._factory.node(Element.Attribute{ + return frame._factory.node(self, Element.Attribute{ ._name = name.str, ._value = String.empty, ._element = null, @@ -522,52 +511,26 @@ fn getImplementation(self: *Document, frame: *Frame) !*DOMImplementation { } fn createDocumentFragment(self: *Document, frame: *Frame) !*Node.DocumentFragment { - const frag = try Node.DocumentFragment.init(frame); - // Track owner document if it's not the main document - if (self != frame.document) { - try frame.setNodeOwnerDocument(frag.asNode(), self); - } - return frag; + return Node.DocumentFragment.init(self, frame); } -pub fn createComment(self: *Document, data: []const u8, frame: *Frame) !*Node { - const node = try Frame.node_factory.createComment(frame, data); - // Track owner document if it's not the main document - if (self != frame.document) { - try frame.setNodeOwnerDocument(node, self); - } - return node; +pub fn createComment(self: *Document, data: []const u8) !*Node { + return Frame.node_factory.createComment(self, data); } -pub fn createTextNode(self: *Document, data: []const u8, frame: *Frame) !*Node { - const node = try Frame.node_factory.createTextNode(frame, data); - // Track owner document if it's not the main document - if (self != frame.document) { - try frame.setNodeOwnerDocument(node, self); - } - return node; +pub fn createTextNode(self: *Document, data: []const u8) !*Node { + return Frame.node_factory.createTextNode(self, data); } -pub fn createCDATASection(self: *Document, data: []const u8, frame: *Frame) !*Node { - const node = switch (self._type) { - .html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument - .xml => try Frame.node_factory.createCDATASection(frame, data), - .generic => try Frame.node_factory.createCDATASection(frame, data), +pub fn createCDATASection(self: *Document, data: []const u8) !*Node { + return switch (self._type) { + .html => error.NotSupported, // cannot create a CDataSection in an HTMLDocument + .xml, .generic => Frame.node_factory.createCDATASection(self, data), }; - // Track owner document if it's not the main document - if (self != frame.document) { - try frame.setNodeOwnerDocument(node, self); - } - return node; } -pub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8, frame: *Frame) !*Node { - const node = try Frame.node_factory.createProcessingInstruction(frame, target, data); - // Track owner document if it's not the main document - if (self != frame.document) { - try frame.setNodeOwnerDocument(node, self); - } - return node; +pub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8) !*Node { + return Frame.node_factory.createProcessingInstruction(self, target, data); } const Range = @import("Range.zig"); @@ -801,7 +764,7 @@ fn adoptNode(self: *Document, node: *Node, frame: *Frame) !*Node { return error.HierarchyError; } - const old_owner = node.ownerDocument(frame) orelse frame.document; + const old_owner = node.ownerDocument(frame).?; if (node._parent) |parent| { frame.removeNode(parent, node, .{ .reconnect_to = null }); @@ -814,12 +777,12 @@ fn adoptNode(self: *Document, node: *Node, frame: *Frame) !*Node { return node; } -fn importNode(_: *const Document, node: *Node, deep_: ?bool, frame: *Frame) !*Node { +fn importNode(self: *const Document, node: *Node, deep_: ?bool, frame: *Frame) !*Node { if (node._type == .document) { return error.NotSupported; } - return node.cloneNode(deep_, frame); + return node.cloneNodeInto(deep_ orelse false, self, frame); } pub fn append(self: *Document, nodes: []const Node.NodeOrText, frame: *Frame) !void { @@ -829,7 +792,7 @@ pub fn append(self: *Document, nodes: []const Node.NodeOrText, frame: *Frame) !v frame.domChanged(); for (nodes) |node_or_text| { - const child = try node_or_text.toNode(frame); + const child = try node_or_text.toNode(self); // DocumentFragments are special - append all their children if (child.is(Node.DocumentFragment)) |_| { @@ -855,7 +818,7 @@ pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, frame: *Frame) ! var i = nodes.len; while (i > 0) { i -= 1; - const child = try nodes[i].toNode(frame); + const child = try nodes[i].toNode(self); // DocumentFragments are special - need to insert all their children if (child.is(Node.DocumentFragment)) |frag| { @@ -1089,7 +1052,7 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool // Our implementation is hacky. We'll write to a DocumentFragment, then // append its children. - const fragment = try Node.DocumentFragment.init(frame); + const fragment = try Node.DocumentFragment.init(self, frame); const fragment_node = fragment.asNode(); const previous_parse_mode = frame._parse_mode; @@ -1517,9 +1480,9 @@ fn _injectBlank(self: *Document, frame: *Frame) !void { std.debug.assert(self.asNode()._first_child == null); } - const html = try Frame.node_factory.createElementNS(frame, .html, "html", null); - const head = try Frame.node_factory.createElementNS(frame, .html, "head", null); - const body = try Frame.node_factory.createElementNS(frame, .html, "body", null); + const html = try Frame.node_factory.createElementNS(self, .html, "html", null); + const head = try Frame.node_factory.createElementNS(self, .html, "head", null); + const body = try Frame.node_factory.createElementNS(self, .html, "body", null); try frame.appendNode(html, head, .{}); try frame.appendNode(html, body, .{}); try frame.appendNode(self.asNode(), html, .{}); @@ -1542,12 +1505,7 @@ pub const JsApi = struct { pub const constructor = bridge.constructor(_constructor, .{}); fn _constructor(frame: *Frame) !*Document { - return frame._factory.node(Document{ - ._proto = undefined, - ._type = .generic, - ._url = "about:blank", - ._charset = "UTF-8", - }); + return frame._factory.genericDocument(.{ .url = "about:blank", .charset = "UTF-8" }); } pub const onselectionchange = bridge.accessor(Document.getOnSelectionChange, Document.setOnSelectionChange, .{}); diff --git a/src/browser/webapi/DocumentFragment.zig b/src/browser/webapi/DocumentFragment.zig index 46580c6df..a1d0f58a7 100644 --- a/src/browser/webapi/DocumentFragment.zig +++ b/src/browser/webapi/DocumentFragment.zig @@ -54,8 +54,8 @@ pub fn as(self: *DocumentFragment, comptime T: type) *T { return self.is(T).?; } -pub fn init(frame: *Frame) !*DocumentFragment { - return frame._factory.node(DocumentFragment{ +pub fn init(document: *const Node.Document, frame: *Frame) !*DocumentFragment { + return frame._factory.node(document, DocumentFragment{ ._type = .generic, ._proto = undefined, }); @@ -129,7 +129,7 @@ fn getChildElementCount(self: *DocumentFragment) usize { pub fn append(self: *DocumentFragment, nodes: []const Node.NodeOrText, frame: *Frame) !void { const parent = self.asNode(); for (nodes) |node_or_text| { - const child = try node_or_text.toNode(frame); + const child = try node_or_text.toNode(parent.getDocument(frame)); _ = try parent.appendChild(child, frame); } } @@ -139,7 +139,7 @@ pub fn prepend(self: *DocumentFragment, nodes: []const Node.NodeOrText, frame: * var i = nodes.len; while (i > 0) { i -= 1; - const child = try nodes[i].toNode(frame); + const child = try nodes[i].toNode(parent.getDocument(frame)); _ = try parent.insertBefore(child, parent.firstChild(), frame); } } @@ -171,14 +171,14 @@ pub fn setHTMLUnsafe(self: *DocumentFragment, html: []const u8, frame: *Frame) ! return parent.setHTML(html, .{ .allow_declarative_shadow = true }, frame); } -pub fn cloneFragment(self: *DocumentFragment, deep: bool, frame: *Frame) !*Node { - const fragment = try DocumentFragment.init(frame); +pub fn cloneFragment(self: *DocumentFragment, deep: bool, document: *const Node.Document, frame: *Frame) !*Node { + const fragment = try DocumentFragment.init(document, frame); const fragment_node = fragment.asNode(); if (deep) { var child_it = self.asNode().childrenIterator(); while (child_it.next()) |child| { - if (try child.cloneNodeForAppending(true, frame)) |cloned_child| { + if (try child.cloneNodeForAppending(true, document, frame)) |cloned_child| { try frame.appendNode(fragment_node, cloned_child, .{}); } } @@ -196,7 +196,10 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; - pub const constructor = bridge.constructor(DocumentFragment.init, .{}); + pub const constructor = bridge.constructor(_constructor, .{}); + fn _constructor(frame: *Frame) !*DocumentFragment { + return init(frame.document, frame); + } pub const getElementById = bridge.function(_getElementById, .{}); fn _getElementById(self: *DocumentFragment, value_: ?js.Value) !?*Element { diff --git a/src/browser/webapi/DocumentType.zig b/src/browser/webapi/DocumentType.zig index b04df318b..b38837adc 100644 --- a/src/browser/webapi/DocumentType.zig +++ b/src/browser/webapi/DocumentType.zig @@ -32,13 +32,13 @@ _name: []const u8, _public_id: []const u8, _system_id: []const u8, -pub fn init(qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, frame: *Frame) !*DocumentType { +pub fn init(document: *const Node.Document, qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, frame: *Frame) !*DocumentType { const name = try frame.dupeString(qualified_name); // Firefox converts null to the string "null", not empty string const pub_id = if (public_id) |p| try frame.dupeString(p) else "null"; const sys_id = if (system_id) |s| try frame.dupeString(s) else "null"; - return frame._factory.node(DocumentType{ + return frame._factory.node(document, DocumentType{ ._proto = undefined, ._name = name, ._public_id = pub_id, @@ -72,8 +72,8 @@ pub fn isEqualNode(self: *const DocumentType, other: *const DocumentType) bool { std.mem.eql(u8, self._system_id, other._system_id); } -pub fn clone(self: *const DocumentType, frame: *Frame) !*DocumentType { - return .init(self._name, self._public_id, self._system_id, frame); +pub fn clone(self: *const DocumentType, document: *const Node.Document, frame: *Frame) !*DocumentType { + return .init(document, self._name, self._public_id, self._system_id, frame); } pub fn remove(self: *DocumentType, frame: *Frame) !void { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index bb760f157..4a5d7e526 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -221,6 +221,10 @@ pub fn as(self: *Element, comptime T: type) *T { return self.is(T).?; } +pub fn getDocument(self: *Element, frame: *const Frame) *Node.Document { + return self.asNode().getDocument(frame); +} + pub fn asNode(self: *Element) *Node { return Factory.protoOf(self); } @@ -568,7 +572,7 @@ pub fn setOuterHTML(self: *Element, html: []const u8, frame: *Frame) !void { var fragment: ?*Node = null; if (html.len > 0) { - const frag = (try Node.DocumentFragment.init(frame)).asNode(); + const frag = (try Node.DocumentFragment.init(node.getDocument(frame), frame)).asNode(); // The parent is the parse context (a fragment parent means body). try Frame.parse.fragment(frame, frag, html, .{ .context = parent.is(Element) }); fragment = frag; @@ -936,7 +940,7 @@ pub fn insertAdjacentText( error.AdjacentNoParent => return, else => return err, }; - const text_node = try Frame.node_factory.createTextNode(frame, data); + const text_node = try Frame.node_factory.createTextNode(self.getDocument(frame), data); _ = try target_node.insertBefore(text_node, prev_node, frame); } @@ -1128,7 +1132,7 @@ pub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame var rm_ref_node = true; for (nodes) |node_or_text| { - const child = try node_or_text.toNode(frame); + const child = try node_or_text.toNode(self.getDocument(frame)); // If a child is the ref node. We keep it at its own current position. if (child == ref_node) { @@ -1290,7 +1294,7 @@ pub fn before(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame) !vo const parent = node.parentNode() orelse return; for (nodes) |node_or_text| { - const child = try node_or_text.toNode(frame); + const child = try node_or_text.toNode(self.getDocument(frame)); _ = try parent.insertBefore(child, node, frame); } } @@ -1301,7 +1305,7 @@ pub fn after(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame) !voi const viable_next = Node.NodeOrText.viableNextSibling(node, nodes); for (nodes) |node_or_text| { - const child = try node_or_text.toNode(frame); + const child = try node_or_text.toNode(self.getDocument(frame)); _ = try parent.insertBefore(child, viable_next, frame); } } @@ -1868,9 +1872,9 @@ pub fn getElementsByClassName(self: *Element, class_name: []const u8, frame: *Fr return self.asNode().getElementsByClassName(class_name, frame); } -pub fn clone(self: *Element, deep: bool, frame: *Frame) !*Node { +pub fn clone(self: *Element, deep: bool, document: *const Node.Document, frame: *Frame) !*Node { const tag_name = self.getTagNameDump(); - const node = try Frame.node_factory.createElementNS(frame, self._namespace, tag_name, &self._attributes); + const node = try Frame.node_factory.createElementNS(document, self._namespace, tag_name, &self._attributes); // A namespace outside the built-in set lives in a side table; the clone // must report the same namespaceURI. @@ -1901,7 +1905,7 @@ pub fn clone(self: *Element, deep: bool, frame: *Frame) !*Node { const cloned_shadow_node = cloned_shadow.asNode(); var shadow_child_it = shadow.asNode().childrenIterator(); while (shadow_child_it.next()) |child| { - if (try child.cloneNodeForAppending(true, frame)) |cloned_child| { + if (try child.cloneNodeForAppending(true, document, frame)) |cloned_child| { try frame.appendNode(cloned_shadow_node, cloned_child, .{}); } } @@ -1911,7 +1915,7 @@ pub fn clone(self: *Element, deep: bool, frame: *Frame) !*Node { if (deep) { var child_it = self.asNode().childrenIterator(); while (child_it.next()) |child| { - if (try child.cloneNodeForAppending(true, frame)) |cloned_child| { + if (try child.cloneNodeForAppending(true, document, frame)) |cloned_child| { try frame.appendNode(node, cloned_child, .{}); } } diff --git a/src/browser/webapi/HTMLDocument.zig b/src/browser/webapi/HTMLDocument.zig index 3f1ef67fe..910ca4aa7 100644 --- a/src/browser/webapi/HTMLDocument.zig +++ b/src/browser/webapi/HTMLDocument.zig @@ -71,7 +71,7 @@ pub fn setBody(self: *HTMLDocument, html: []const u8, frame: *Frame) !void { // Build a fresh holding the parsed HTML as its children. Fragment // parsing strips any // wrappers the author included. - const new_body_node = try Frame.node_factory.createElementNS(frame, .html, "body", null); + const new_body_node = try Frame.node_factory.createElementNS(self._proto, .html, "body", null); if (html.len > 0) { try Frame.parse.htmlAsChildren(frame, new_body_node, html); } @@ -158,7 +158,7 @@ fn setTitle(self: *HTMLDocument, title: []const u8, frame: *Frame) !void { } // No title element found, create one - const title_node = try Frame.node_factory.createElementNS(frame, .html, "title", null); + const title_node = try Frame.node_factory.createElementNS(self._proto, .html, "title", null); const title_element = title_node.as(Element); // Only add text if non-empty @@ -242,7 +242,7 @@ fn getDocType(self: *HTMLDocument, frame: *Frame) !*DocumentType { } } - self._document_type = try frame._factory.node(DocumentType{ + self._document_type = try frame._factory.node(self._proto, DocumentType{ ._proto = undefined, ._name = "html", ._public_id = "", diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 2a3e2708b..2fd8ec394 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -52,14 +52,12 @@ _parent: ?*Node = null, _first_child: ?*Node = null, _next: ?*Node = null, _prev: ?*Node = null, +_owner: u32 = undefined, // owner document index // In debug, set so that we can check that we have a proper contiguous block // of memory for the entire chain (and thus, simple pointer arithmetics will // work to resolve the proto). _proto_canary: if (lp.IS_DEBUG) *EventTarget else void = undefined, -// Lookup for nodes that have a different owner document than frame.document -pub const OwnerDocumentLookup = std.AutoHashMapUnmanaged(*Node, *Document); - pub const Type = enum(u8) { cdata, element, @@ -708,48 +706,24 @@ pub fn ownerDocument(self: *const Node, frame: *const Frame) ?*Document { return null; } - // An attribute node has no parent; its owner follows its element's - // (including across adoption into another document). + // An attribute node follows its element (including across adoption). if (self._type == .attribute) { if (self.subtype(Element.Attribute)._element) |element| { return element.asNode().ownerDocument(frame); } } - // The root of the tree that a node belongs to is its owner. - var current = self; - while (current._parent) |parent| { - current = parent; - } - - // If the root is a document, then that's our owner. - if (current._type == .document) { - return current.subtype(Document); - } - - // A shadow tree's root is a parent-less ShadowRoot fragment; its owner - // is the host's owner document. - if (current._type == .document_fragment) { - if (current.subtype(DocumentFragment).is(ShadowRoot)) |sr| { - return sr._host.asNode().ownerDocument(frame); - } - } - - // Otherwise, this is a detached node. Check if it has a specific owner - // document registered (for nodes created via non-main documents). - if (frame._node_owner_documents.get(@constCast(self))) |owner| { - return owner; - } - - // Default to the main document for detached nodes without a specific owner. - return frame.document; + // The table is the browser's, so any frame of it will do. + return frame._session.browser.documents.get(self._owner); } -fn ownerDocumentIncludingSelf(self: *const Node, frame: *const Frame) ?*Document { +// The spec's "node document": every node has one, a document's is itself. +// `frame` only reaches the registry; any frame will do. +pub fn getDocument(self: *const Node, frame: *const Frame) *Document { if (self._type == .document) { return self.subtype(Document); } - return self.ownerDocument(frame); + return self.ownerDocument(frame).?; } // Returns the Frame that owns this node's tree, or null when the node's @@ -757,9 +731,11 @@ fn ownerDocumentIncludingSelf(self: *const Node, frame: *const Frame) ?*Document // whose frame has since navigated away. Used to tie per-frame state (the // StyleManager, live-collection versions, the event manager, ...) to the // right frame: cross-realm callers must not use the calling frame's. +// +// `frame` only locates the document table; it is never returned as a +// fallback. pub fn ownerFrame(self: *const Node, frame: *const Frame) ?*Frame { - const doc = self.ownerDocumentIncludingSelf(frame) orelse return null; - return doc._frame; + return self.getDocument(frame)._frame; } pub const ResolveURLOpts = struct { @@ -770,11 +746,10 @@ pub const ResolveURLOpts = struct { // Uses the document's charset for query string encoding (with NCR fallback for unmappable chars). pub fn resolveURL(self: *const Node, url: anytype, frame: *Frame, opts: ResolveURLOpts) ![:0]const u8 { const allocator = opts.allocator orelse frame.call_arena; - const doc: ?*const Document = self.ownerDocumentIncludingSelf(frame); - const encoding = if (doc) |d| d.getCharset() else frame.charset; + const doc = self.getDocument(frame); // A frameless document (DOMParser, XHR) has no ; its URL is the base. - const base = if (self.ownerFrame(frame)) |owner| owner.base() else if (doc) |d| d.getURL(frame) else frame.url; - return URL.resolve(allocator, base, url, .{ .encoding = encoding }); + const base = if (doc._frame) |owner| owner.base() else doc.getURL(frame); + return URL.resolve(allocator, base, url, .{ .encoding = doc.getCharset() }); } // Same as `resolveURL` but can't return `TypeError`, this is needed for multiple @@ -787,10 +762,7 @@ pub fn resolveURLReflect(self: *const Node, url: []const u8, frame: *Frame, opts } pub fn isSameDocumentAs(self: *const Node, other: *const Node, frame: *const Frame) bool { - // Get the root document for each node - const self_doc = self.ownerDocumentIncludingSelf(frame); - const other_doc = other.ownerDocumentIncludingSelf(frame); - return self_doc == other_doc; + return self.getDocument(frame) == other.getDocument(frame); } pub fn hasChildNodes(self: *const Node) bool { @@ -1206,25 +1178,31 @@ const CloneError = error{ CompilationError, JsException, ExecutionTerminated, + QuotaExceeded, }; pub fn cloneNode(self: *Node, deep_: ?bool, frame: *Frame) CloneError!*Node { + return self.cloneNodeInto(deep_ orelse false, self.getDocument(frame), frame); +} + +// The spec's clone: the copy belongs to `document`, importNode's target or +// the original's own document. +pub fn cloneNodeInto(self: *Node, deep: bool, document: *const Document, frame: *Frame) CloneError!*Node { if (self.is(ShadowRoot) != null) { return error.NotSupported; } - const deep = deep_ orelse false; switch (self._type) { .cdata => { const cd = self.subtype(CData); const data = cd.getData().str(); return switch (cd._type) { - .text => Frame.node_factory.createTextNode(frame, data), - .cdata_section => Frame.node_factory.createCDATASection(frame, data), - .comment => Frame.node_factory.createComment(frame, data), - .processing_instruction => Frame.node_factory.createProcessingInstruction(frame, cd.subtype(CData.ProcessingInstruction)._target, data), + .text => Frame.node_factory.createTextNode(document, data), + .cdata_section => Frame.node_factory.createCDATASection(document, data), + .comment => Frame.node_factory.createComment(document, data), + .processing_instruction => Frame.node_factory.createProcessingInstruction(document, cd.subtype(CData.ProcessingInstruction)._target, data), }; }, - .element => return self.subtype(Element).clone(deep, frame), + .element => return self.subtype(Element).clone(deep, document, frame), .document => { const doc = self.subtype(Document); const cloned = switch (doc._type) { @@ -1237,7 +1215,7 @@ pub fn cloneNode(self: *Node, deep_: ?bool, frame: *Frame) CloneError!*Node { if (deep) { var child_it = self.childrenIterator(); while (child_it.next()) |child| { - if (try child.cloneNodeForAppending(true, frame)) |cloned_child| { + if (try child.cloneNodeForAppending(true, cloned, frame)) |cloned_child| { _ = cloned.asNode().appendChild(cloned_child, frame) catch return error.CloneError; } } @@ -1245,12 +1223,12 @@ pub fn cloneNode(self: *Node, deep_: ?bool, frame: *Frame) CloneError!*Node { return cloned.asNode(); }, .document_type => { - const cloned = self.subtype(DocumentType).clone(frame) catch return error.CloneError; + const cloned = self.subtype(DocumentType).clone(document, frame) catch return error.CloneError; return cloned.asNode(); }, - .document_fragment => return self.subtype(DocumentFragment).cloneFragment(deep, frame), + .document_fragment => return self.subtype(DocumentFragment).cloneFragment(deep, document, frame), .attribute => { - const cloned = self.subtype(Element.Attribute).clone(frame) catch return error.CloneError; + const cloned = self.subtype(Element.Attribute).clone(document, frame) catch return error.CloneError; return cloned.asNode(); }, } @@ -1262,7 +1240,7 @@ pub fn cloneNode(self: *Node, deep_: ?bool, frame: *Frame) CloneError!*Node { /// /// This helper is used when iterating over children to clone them. The typical pattern is: /// while (child_it.next()) |child| { -/// if (try child.cloneNodeForAppending(true, frame)) |cloned| { +/// if (try child.cloneNodeForAppending(true, document, frame)) |cloned| { /// try frame.appendNode(parent, cloned, opts); /// } /// } @@ -1271,8 +1249,8 @@ pub fn cloneNode(self: *Node, deep_: ?bool, frame: *Frame) CloneError!*Node { /// constructor (which runs during cloning per the HTML spec) explicitly attaches the element /// somewhere. In that case, we respect the constructor's decision and return null to signal /// that the cloned node should not be appended to our intended parent. -pub fn cloneNodeForAppending(self: *Node, deep: bool, frame: *Frame) CloneError!?*Node { - const cloned = try self.cloneNode(deep, frame); +pub fn cloneNodeForAppending(self: *Node, deep: bool, document: *const Document, frame: *Frame) CloneError!?*Node { + const cloned = try self.cloneNodeInto(deep, document, frame); if (cloned._parent != null) { return null; } @@ -1479,7 +1457,7 @@ pub fn getElementsByClassName(self: *Node, class_name: []const u8, frame: *Frame try class_names.append(arena, try frame.dupeString(name)); } - const quirks = if (self.ownerDocumentIncludingSelf(frame)) |doc| doc.isQuirksMode() else false; + const quirks = self.getDocument(frame).isQuirksMode(); return collections.NodeLive(.class_name).init(self, .{ .names = class_names.items, .case_insensitive = quirks, @@ -1491,11 +1469,11 @@ pub fn getElementsByClassName(self: *Node, class_name: []const u8, frame: *Frame // observe its later siblings inserted (and can remove them before they run). pub fn appendNodes(self: *Node, nodes: []const NodeOrText, frame: *Frame) !void { if (nodes.len == 1) { - const child = try nodes[0].toNode(frame); + const child = try nodes[0].toNode(self.getDocument(frame)); _ = try self.appendChild(child, frame); return; } - const fragment = try DocumentFragment.init(frame); + const fragment = try DocumentFragment.init(self.getDocument(frame), frame); const fragment_node = fragment.asNode(); // The fragment is internal — JS never sees it, and no mutation record // targets it — so it can be reclaimed once its children have moved out. @@ -1503,7 +1481,7 @@ pub fn appendNodes(self: *Node, nodes: []const NodeOrText, frame: *Frame) !void // it (per spec), so it must live on. defer if (fragment_node.firstChild() == null) frame._factory.destroy(fragment); for (nodes) |node_or_text| { - const child = try node_or_text.toNode(frame); + const child = try node_or_text.toNode(self.getDocument(frame)); _ = try fragment_node.appendChild(child, frame); } _ = try self.appendChild(fragment_node, frame); @@ -1511,15 +1489,15 @@ pub fn appendNodes(self: *Node, nodes: []const NodeOrText, frame: *Frame) !void pub fn prependNodes(self: *Node, nodes: []const NodeOrText, frame: *Frame) !void { if (nodes.len == 1) { - const child = try nodes[0].toNode(frame); + const child = try nodes[0].toNode(self.getDocument(frame)); _ = try self.insertBefore(child, self.firstChild(), frame); return; } - const fragment = try DocumentFragment.init(frame); + const fragment = try DocumentFragment.init(self.getDocument(frame), frame); const fragment_node = fragment.asNode(); defer if (fragment_node.firstChild() == null) frame._factory.destroy(fragment); for (nodes) |node_or_text| { - const child = try node_or_text.toNode(frame); + const child = try node_or_text.toNode(self.getDocument(frame)); _ = try fragment_node.appendChild(child, frame); } // The reference child is evaluated after converting nodes into the @@ -1535,7 +1513,7 @@ pub fn replaceChildren(self: *Node, nodes: []const NodeOrText, frame: *Frame) !v var children_to_add: std.ArrayList(*Node) = .empty; for (nodes) |node_or_text| { - const child = try node_or_text.toNode(frame); + const child = try node_or_text.toNode(self.getDocument(frame)); // DocumentFragments contribute their children, not themselves if (child.is(DocumentFragment)) |frag| { @@ -1790,7 +1768,7 @@ pub const JsApi = struct { pub const baseURI = bridge.accessor(_baseURI, null, .{}); fn _baseURI(self: *Node, frame: *const Frame) []const u8 { - const doc = self.ownerDocumentIncludingSelf(frame) orelse return frame.base(); + const doc = self.getDocument(frame); if (doc._frame) |doc_frame| { return doc_frame.base(); } @@ -1842,10 +1820,10 @@ pub const NodeOrText = union(enum) { } } - pub fn toNode(self: *const NodeOrText, frame: *Frame) !*Node { + pub fn toNode(self: *const NodeOrText, document: *const Document) !*Node { return switch (self.*) { .node => |n| n, - .text => |txt| Frame.node_factory.createTextNode(frame, txt), + .text => |txt| Frame.node_factory.createTextNode(document, txt), }; } diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig index 6fa5aeb55..349837467 100644 --- a/src/browser/webapi/Range.zig +++ b/src/browser/webapi/Range.zig @@ -479,7 +479,7 @@ fn nodeContained(self: *const Range, node: *Node) bool { } fn cloneContents(self: *const Range, frame: *Frame) !*DocumentFragment { - const fragment = try DocumentFragment.init(frame); + const fragment = try DocumentFragment.init(self._proto._start_container.getDocument(frame), frame); if (self._proto.getCollapsed()) return fragment; try cloneContentsBetween( @@ -499,7 +499,7 @@ fn cloneContentsBetween(frame: *Frame, out: *Node, start_node: *Node, start_offs if (start_node == end_node) { if (start_node.is(Node.CData)) |cdata| { const data = cdata.getData().str(); - const cloned = (try start_node.cloneNodeForAppending(false, frame)) orelse return; + const cloned = (try start_node.cloneNodeForAppending(false, out.getDocument(frame), frame)) orelse return; try cloned.setData(data[byteOffset(data, start_offset)..byteOffset(data, end_offset)], frame); _ = try out.appendChild(cloned, frame); return; @@ -522,11 +522,11 @@ fn cloneContentsBetween(frame: *Frame, out: *Node, start_node: *Node, start_offs if (c.is(Node.CData)) |cdata| { // c is the start node itself. const data = cdata.getData().str(); - const cloned = (try c.cloneNodeForAppending(false, frame)) orelse continue; + const cloned = (try c.cloneNodeForAppending(false, out.getDocument(frame), frame)) orelse continue; try cloned.setData(data[byteOffset(data, start_offset)..], frame); _ = try out.appendChild(cloned, frame); } else { - const cloned = (try c.cloneNodeForAppending(false, frame)) orelse continue; + const cloned = (try c.cloneNodeForAppending(false, out.getDocument(frame), frame)) orelse continue; _ = try out.appendChild(cloned, frame); try cloneContentsBetween(frame, cloned, start_node, start_offset, c, c.getLength()); } @@ -535,11 +535,11 @@ fn cloneContentsBetween(frame: *Frame, out: *Node, start_node: *Node, start_offs if (c.is(Node.CData)) |cdata| { // c is the end node itself. const data = cdata.getData().str(); - const cloned = (try c.cloneNodeForAppending(false, frame)) orelse continue; + const cloned = (try c.cloneNodeForAppending(false, out.getDocument(frame), frame)) orelse continue; try cloned.setData(data[0..byteOffset(data, end_offset)], frame); _ = try out.appendChild(cloned, frame); } else { - const cloned = (try c.cloneNodeForAppending(false, frame)) orelse continue; + const cloned = (try c.cloneNodeForAppending(false, out.getDocument(frame), frame)) orelse continue; _ = try out.appendChild(cloned, frame); try cloneContentsBetween(frame, cloned, c, 0, end_node, end_offset); } @@ -547,7 +547,7 @@ fn cloneContentsBetween(frame: *Frame, out: *Node, start_node: *Node, start_offs if (c._type == .document_type) { return error.HierarchyError; } - const cloned = (try c.cloneNodeForAppending(true, frame)) orelse continue; + const cloned = (try c.cloneNodeForAppending(true, out.getDocument(frame), frame)) orelse continue; _ = try out.appendChild(cloned, frame); } } @@ -560,7 +560,7 @@ fn containedBetween(node: *Node, start_node: *Node, start_offset: u32, end_node: } fn extractContents(self: *Range, frame: *Frame) !*DocumentFragment { - const fragment = try DocumentFragment.init(frame); + const fragment = try DocumentFragment.init(self._proto._start_container.getDocument(frame), frame); if (self._proto.getCollapsed()) return fragment; frame.domChanged(); @@ -599,7 +599,7 @@ fn extractContentsBetween(frame: *Frame, out: *Node, start_node: *Node, start_of if (start_node == end_node) { if (start_node.is(Node.CData)) |cdata| { const data = cdata.getData().str(); - const cloned = (try start_node.cloneNodeForAppending(false, frame)) orelse return; + const cloned = (try start_node.cloneNodeForAppending(false, out.getDocument(frame), frame)) orelse return; try cloned.setData(data[byteOffset(data, start_offset)..byteOffset(data, end_offset)], frame); _ = try out.appendChild(cloned, frame); try cdata.replaceData(start_offset, end_offset - start_offset, "", frame); @@ -635,13 +635,13 @@ fn extractContentsBetween(frame: *Frame, out: *Node, start_node: *Node, start_of // c is the start node itself. const data = cdata.getData().str(); const byte_start = byteOffset(data, start_offset); - const cloned = (try c.cloneNodeForAppending(false, frame)) orelse return; + const cloned = (try c.cloneNodeForAppending(false, out.getDocument(frame), frame)) orelse return; try cloned.setData(data[byte_start..], frame); _ = try out.appendChild(cloned, frame); const length: u32 = @intCast(cdata.getLength()); try cdata.replaceData(start_offset, length - start_offset, "", frame); } else { - const cloned = (try c.cloneNodeForAppending(false, frame)) orelse return; + const cloned = (try c.cloneNodeForAppending(false, out.getDocument(frame), frame)) orelse return; _ = try out.appendChild(cloned, frame); try extractContentsBetween(frame, cloned, start_node, start_offset, c, c.getLength()); } @@ -655,12 +655,12 @@ fn extractContentsBetween(frame: *Frame, out: *Node, start_node: *Node, start_of if (c.is(Node.CData)) |cdata| { // c is the end node itself. const data = cdata.getData().str(); - const cloned = (try c.cloneNodeForAppending(false, frame)) orelse return; + const cloned = (try c.cloneNodeForAppending(false, out.getDocument(frame), frame)) orelse return; try cloned.setData(data[0..byteOffset(data, end_offset)], frame); _ = try out.appendChild(cloned, frame); try cdata.replaceData(0, end_offset, "", frame); } else { - const cloned = (try c.cloneNodeForAppending(false, frame)) orelse return; + const cloned = (try c.cloneNodeForAppending(false, out.getDocument(frame), frame)) orelse return; _ = try out.appendChild(cloned, frame); try extractContentsBetween(frame, cloned, c, 0, end_node, end_offset); } @@ -689,7 +689,8 @@ fn createContextualFragment(self: *const Range, html: []const u8, frame: *Frame) context_node = context_node.parentNode() orelse context_node; } - const fragment = try DocumentFragment.init(frame); + const document = context_node.getDocument(frame); + const fragment = try DocumentFragment.init(document, frame); if (html.len == 0) { return fragment; @@ -698,9 +699,9 @@ fn createContextualFragment(self: *const Range, html: []const u8, frame: *Frame) // Create a temporary element of the same type as the context for parsing // This preserves the parsing context without modifying the original node const temp_node = if (context_node.is(Node.Element)) |el| - try Frame.node_factory.createElementNS(frame, el._namespace, el.getTagNameLower(), null) + try Frame.node_factory.createElementNS(document, el._namespace, el.getTagNameLower(), null) else - try Frame.node_factory.createElementNS(frame, .html, "div", null); + try Frame.node_factory.createElementNS(document, .html, "div", null); try Frame.parse.contextualFragment(frame, temp_node, html); diff --git a/src/browser/webapi/ShadowRoot.zig b/src/browser/webapi/ShadowRoot.zig index f2870d7c9..99f6c1566 100644 --- a/src/browser/webapi/ShadowRoot.zig +++ b/src/browser/webapi/ShadowRoot.zig @@ -62,7 +62,7 @@ _removed_ids: std.StringHashMapUnmanaged(void) = .{}, _adopted_style_sheets: ?js.Object.Global = null, pub fn init(host: *Element, opts: AttachOptions, frame: *Frame) !*ShadowRoot { - return frame._factory.documentFragment(ShadowRoot{ + return frame._factory.documentFragment(host.getDocument(frame), ShadowRoot{ ._proto = undefined, ._mode = opts.mode, ._host = host, diff --git a/src/browser/webapi/cdata/Comment.zig b/src/browser/webapi/cdata/Comment.zig index 37ea7d2e2..703c15c4a 100644 --- a/src/browser/webapi/cdata/Comment.zig +++ b/src/browser/webapi/cdata/Comment.zig @@ -31,7 +31,7 @@ _pad: bool = false, _proto_canary: if (lp.IS_DEBUG) *CData else void = undefined, pub fn init(str: ?js.NullableString, frame: *Frame) !*Comment { - const node = try Frame.node_factory.createComment(frame, if (str) |s| s.value else ""); + const node = try Frame.node_factory.createComment(frame.document, if (str) |s| s.value else ""); return node.as(Comment); } diff --git a/src/browser/webapi/cdata/Text.zig b/src/browser/webapi/cdata/Text.zig index 0e00d5754..7b2ae99fd 100644 --- a/src/browser/webapi/cdata/Text.zig +++ b/src/browser/webapi/cdata/Text.zig @@ -40,7 +40,7 @@ pub fn asCData(self: *const Text) *CData { } pub fn init(str: ?js.NullableString, frame: *Frame) !*Text { - const node = try Frame.node_factory.createTextNode(frame, if (str) |s| s.value else ""); + const node = try Frame.node_factory.createTextNode(frame.document, if (str) |s| s.value else ""); return node.as(Text); } @@ -91,7 +91,7 @@ pub fn splitText(self: *Text, offset: usize, frame: *Frame) !*Text { const byte_offset = CData.utf16OffsetToUtf8(data, offset) catch return error.IndexSizeError; const new_data = data[byte_offset..]; - const new_node = try Frame.node_factory.createTextNode(frame, new_data); + const new_node = try Frame.node_factory.createTextNode(Factory.protoOf(self).asNode().getDocument(frame), new_data); const new_text = new_node.as(Text); const node = Factory.protoOf(self).asNode(); diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index 3423ece02..42d977ffc 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -88,20 +88,12 @@ pub fn isEqualNode(self: *const Attribute, other: *const Attribute) bool { return self.getName().eql(other.getName()) and self.getValue().eql(other.getValue()); } -pub fn clone(self: *const Attribute, frame: *Frame) !*Attribute { - const cloned = try frame._factory.node(Attribute{ +pub fn clone(self: *const Attribute, document: *const Node.Document, frame: *Frame) !*Attribute { + return frame._factory.node(document, Attribute{ ._element = null, ._name = self._name, ._value = self._value, }); - - if (self._element) |el| { - // cloned has no element, we need to store its document - if (el.asNode().ownerDocument(frame)) |doc| { - try frame.setNodeOwnerDocument(cloned.asNode(), doc); - } - } - return cloned; } pub const JsApi = struct { @@ -505,7 +497,9 @@ pub const List = struct { } fn toAttribute(self: *const Entry, element: ?*Element, frame: *Frame) !*Attribute { - return frame._factory.node(Attribute{ + // Without an element, the attribute is the caller's document's. + const document = if (element) |el| el.getDocument(frame) else frame.document; + return frame._factory.node(document, Attribute{ ._element = element, // The entry's bytes outlive the entry itself, so the // Attribute can wrap them without duping. diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 9548db43a..fa6e6f004 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -329,7 +329,7 @@ pub fn getInnerText(self: *HtmlElement, writer: *std.Io.Writer, frame: *Frame) ! } pub fn setInnerText(self: *HtmlElement, text: []const u8, frame: *Frame) !void { - const items = try renderedTextFragment(text, frame); + const items = try renderedTextFragment(self.asNode().getDocument(frame), text, frame); try self.asElement().replaceChildren(items, frame); } @@ -346,7 +346,7 @@ pub fn setOuterText(self: *HtmlElement, text: []const u8, frame: *Frame) !void { const prev = node.previousSibling(); const next = node.nextSibling(); - var items: []const Node.NodeOrText = try renderedTextFragment(text, frame); + var items: []const Node.NodeOrText = try renderedTextFragment(node.getDocument(frame), text, frame); if (items.len == 0) { // A fragment with no node still replaces the element with an empty Text // node so surrounding text can merge with it. @@ -389,7 +389,7 @@ pub fn insertAdjacentHTML( else null; - const fragment = (try DocumentFragment.init(frame)).asNode(); + const fragment = (try DocumentFragment.init(self.asNode().getDocument(frame), frame)).asNode(); try Frame.parse.fragment(frame, fragment, html, .{ .context = context }); const target_node, const prev_node = try self.asNode().findAdjacentNodes(position, .html); @@ -1797,7 +1797,7 @@ fn mergeTextNodes(left_node: *Node, right_node: *Node, frame: *Frame) !bool { return true; } -fn renderedTextFragment(value: []const u8, frame: *Frame) ![]Node.NodeOrText { +fn renderedTextFragment(document: *const Node.Document, value: []const u8, frame: *Frame) ![]Node.NodeOrText { const arena = frame.local_arena; var nodes: std.ArrayList(Node.NodeOrText) = .empty; @@ -1816,7 +1816,7 @@ fn renderedTextFragment(value: []const u8, frame: *Frame) ![]Node.NodeOrText { // break (so "\r\n" is one
but "\n\n" is two). const break_len: usize = if (rest[0] == '\r' and rest.len > 1 and rest[1] == '\n') 2 else 1; - try nodes.append(arena, .{ .node = try Frame.node_factory.createElementNS(frame, .html, "br", null) }); + try nodes.append(arena, .{ .node = try Frame.node_factory.createElementNS(document, .html, "br", null) }); rest = rest[break_len..]; } } diff --git a/src/browser/webapi/element/html/Audio.zig b/src/browser/webapi/element/html/Audio.zig index 0bc707fbb..d0b41ba5d 100644 --- a/src/browser/webapi/element/html/Audio.zig +++ b/src/browser/webapi/element/html/Audio.zig @@ -36,7 +36,7 @@ _pad: bool = false, _proto_canary: if (lp.IS_DEBUG) *Media else void = undefined, pub fn constructor(maybe_url: ?String, frame: *Frame) !*Media { - const node = try Frame.node_factory.createElementNS(frame, .html, "audio", null); + const node = try Frame.node_factory.createElementNS(frame.document, .html, "audio", null); const el = node.as(Element); // Always set to "auto" initially. diff --git a/src/browser/webapi/element/html/Image.zig b/src/browser/webapi/element/html/Image.zig index d153036f3..5a1726823 100644 --- a/src/browser/webapi/element/html/Image.zig +++ b/src/browser/webapi/element/html/Image.zig @@ -21,7 +21,7 @@ _complete: bool = true, _proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined, pub fn constructor(w_: ?u32, h_: ?u32, frame: *Frame) !*Image { - const node = try Frame.node_factory.createElementNS(frame, .html, "img", null); + const node = try Frame.node_factory.createElementNS(frame.document, .html, "img", null); const el = node.as(Element); if (w_) |w| blk: { @@ -97,6 +97,11 @@ fn imageAddedCallback(self: *Image, frame: *Frame) !void { return; } + // A document without a browsing context (DOMParser et al.) loads nothing. + if (self.asElement().getDocument(frame)._frame == null) { + return; + } + self._generation +%= 1; self._complete = true; diff --git a/src/browser/webapi/element/html/Link.zig b/src/browser/webapi/element/html/Link.zig index f09de889a..161fbf0c6 100644 --- a/src/browser/webapi/element/html/Link.zig +++ b/src/browser/webapi/element/html/Link.zig @@ -108,6 +108,11 @@ pub fn linkAddedCallback(self: *Link, frame: *Frame) !void { const element = self.asElement(); + // A document without a browsing context (DOMParser et al.) loads nothing. + if (element.getDocument(frame)._frame == null) { + return; + } + const href = element.getAttributeInterned("href") orelse return; if (href.len == 0) { return; diff --git a/src/browser/webapi/element/html/Meta.zig b/src/browser/webapi/element/html/Meta.zig index a800c762d..f0961ca37 100644 --- a/src/browser/webapi/element/html/Meta.zig +++ b/src/browser/webapi/element/html/Meta.zig @@ -161,6 +161,11 @@ fn immediateRefreshTarget(content: []const u8) ?[]const u8 { pub const Build = struct { pub fn created(node: *Node, frame: *Frame) !void { + // A document without a browsing context has no policy or refresh to set. + if (node.getDocument(frame)._frame == null) { + return; + } + const self = node.as(Meta); const el = self.asElement(); diff --git a/src/browser/webapi/element/html/Template.zig b/src/browser/webapi/element/html/Template.zig index a259a31c1..a2a8fdb5f 100644 --- a/src/browser/webapi/element/html/Template.zig +++ b/src/browser/webapi/element/html/Template.zig @@ -158,7 +158,7 @@ pub const Build = struct { pub fn created(node: *Node, frame: *Frame) !void { const self = node.as(Template); // Create the template content DocumentFragment - self._content = try DocumentFragment.init(frame); + self._content = try DocumentFragment.init(node.getDocument(frame), frame); } // Per the HTML spec's cloning steps for