diff --git a/src/NodeRegistry.zig b/src/NodeRegistry.zig index 4d7eaeae5..f0f217f81 100644 --- a/src/NodeRegistry.zig +++ b/src/NodeRegistry.zig @@ -65,16 +65,17 @@ pub fn reset(self: *NodeRegistry) void { } /// Evict only the nodes owned by `frame`'s page, leaving sibling pages' node -/// IDs valid. Must run before the page's arena is freed — attribution walks -/// each node's live parent chain. +/// IDs valid. Must run before the page's arena is freed — attribution reads +/// each node's document. pub fn resetFrame(self: *NodeRegistry, arena: Allocator, frame: *Frame) void { const page = frame._page; var doomed: std.ArrayListUnmanaged(*Node) = .empty; 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) { + // The document table is the browser's, so a sibling page's node + // resolves correctly through this page's frame. + if (node.dom.getDocument(frame)._page == page) { doomed.append(arena, node) catch return; } } diff --git a/src/browser/Browser.zig b/src/browser/Browser.zig index 5c3d94879..d69b604d6 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; @@ -73,6 +75,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), @@ -123,6 +130,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, @@ -155,6 +163,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..56b7015f1 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,37 @@ 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; +} + // this is a root object pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) { return self.eventTargetWithAllocator(self._slab.allocator(), child); @@ -224,6 +244,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 +339,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 +510,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 +562,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 57b4f626f..362223ea1 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 @@ -484,6 +480,7 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, opts: InitOpts) !void { location.acquireRef(); self.window._location = location; + lp.assert(document._page == page, "unexpected document page", .{}); document._frame = self; if (comptime lp.IS_TEST == false) { @@ -2586,20 +2583,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 cb0cf50f6..c14360002 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). @@ -122,11 +110,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 0bbdc97a0..41d22c6d0 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -60,8 +60,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 @@ -100,6 +101,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{ @@ -159,7 +161,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.inserted_since_checkpoint +|= 1; self.pending_text = .{ @@ -498,9 +500,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; @@ -527,7 +530,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. @@ -552,8 +555,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, @@ -572,8 +574,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, @@ -598,7 +599,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), @@ -808,7 +809,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..2bf956a60 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,26 @@ 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); + const doc = self.ownerDocument(frame); + lp.assert(doc != null, "null node document", .{ .type = self.getNodeType() }); + return doc.?; } // Returns the Frame that owns this node's tree, or null when the node's @@ -757,9 +733,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 +748,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 +764,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 +1180,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 +1217,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 +1225,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 +1242,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 +1251,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 +1459,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 +1471,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 +1483,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 +1491,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 +1515,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 +1770,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 +1822,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..3f41aa6b6 100644 --- a/src/browser/webapi/cdata/Text.zig +++ b/src/browser/webapi/cdata/Text.zig @@ -40,19 +40,19 @@ 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); } // This Text node's own data (getWholeText below spans adjacent Text nodes). pub fn ownData(self: *const Text) []const u8 { - return Factory.protoOf(self)._data.str(); + return self.asCData()._data.str(); } // The concatenated data of the contiguous exclusive Text nodes (adjacent // Text siblings on both sides of this one), in tree order. fn getWholeText(self: *Text, frame: *Frame) ![]const u8 { - const node = Factory.protoOf(self).asNode(); + const node = self.asCData().asNode(); var first = node; while (first.previousSibling()) |prev| { @@ -86,16 +86,17 @@ fn getAssignedSlot(self: *Text, frame: *Frame) ?*Slot { } pub fn splitText(self: *Text, offset: usize, frame: *Frame) !*Text { - const data = Factory.protoOf(self)._data.str(); + const cdata = self.asCData(); + const node = cdata.asNode(); + + const data = cdata._data.str(); 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(node.getDocument(frame), new_data); const new_text = new_node.as(Text); - const node = Factory.protoOf(self).asNode(); - // Per DOM spec splitText: insert first (step 7a), then update ranges (7b-7e), // then truncate original node (step 8). if (node.parentNode()) |parent| { @@ -112,7 +113,7 @@ pub fn splitText(self: *Text, offset: usize, frame: *Frame) !*Text { // Use replaceData instead of setData so live range updates fire // (matters for detached text nodes where steps 7b-7e were skipped). const length = Factory.protoOf(self).getLength(); - try Factory.protoOf(self).replaceData(offset, length - offset, "", frame); + try cdata.replaceData(offset, length - offset, "", frame); return new_text; } diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index 3423ece02..6e91876cf 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 { @@ -212,7 +204,7 @@ pub const List = struct { return self.getEntryWithInternedName(name) != null; } - pub fn getAttribute(self: *const List, name: String, element: ?*Element, frame: *Frame) !?*Attribute { + pub fn getAttribute(self: *const List, name: String, element: *Element, frame: *Frame) !?*Attribute { const entry = (try self.getEntry(name, frame)) orelse return null; return self.getOrCreateAttribute(entry, element, frame); } @@ -220,8 +212,8 @@ pub const List = struct { // Identity map access: a given (list, name) always yields the same // *Attribute until the attribute is removed. The map must be the // element's frame's, not the caller's frame. - pub fn getOrCreateAttribute(self: *const List, entry: *const Entry, element: ?*Element, frame: *Frame) !*Attribute { - const owner = if (element) |el| (el.ownerFrame(frame) orelse frame) else frame; + pub fn getOrCreateAttribute(self: *const List, entry: *const Entry, element: *Element, frame: *Frame) !*Attribute { + const owner = element.ownerFrame(frame) orelse frame; const gop = try owner._attribute_lookup.getOrPut(owner.arena, .{ .list = self, .name = entry._name_ptr }); if (!gop.found_existing) { gop.value_ptr.* = try entry.toAttribute(element, owner); @@ -504,8 +496,8 @@ pub const List = struct { return formatAttribute(self.name(), self.value(), writer); } - fn toAttribute(self: *const Entry, element: ?*Element, frame: *Frame) !*Attribute { - return frame._factory.node(Attribute{ + fn toAttribute(self: *const Entry, element: *Element, frame: *Frame) !*Attribute { + return frame._factory.node(element.getDocument(frame), 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