Merge pull request #3510 from lightpanda-io/node-document

internal: store document in every node
This commit is contained in:
Karl Seguin authored and GitHub committed 2026-09-14 17:41:55 +08:00
commit 72a05c7efa
33 files changed
+661 -500

No files matched your search

+5 -4
View File
@@ -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;
}
}
+9
View File
@@ -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();
+66
View File
@@ -0,0 +1,66 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
// 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 {};
}
+97 -23
View File
@@ -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);
}
+2 -16
View File
@@ -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| {
+2 -1
View File
@@ -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();
}
File diff suppressed because it is too large. Load diff
-17
View File
@@ -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, .{});
+13 -12
View File
@@ -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 }, .{});
+11
View File
@@ -551,6 +551,17 @@
}
</script>
<script id=noBrowsingContext>
{
// The parsed document has no browsing context, so its <base> belongs to
// it alone: the page's base URL is untouched.
const base = document.baseURI;
const doc = new DOMParser().parseFromString('<base href="http://other.example/x/">', 'text/html');
testing.expectEqual('BASE', doc.querySelector('base').tagName);
testing.expectEqual(base, document.baseURI);
}
</script>
<script id=invalidType>
{
let err = null;
+60
View File
@@ -34,3 +34,63 @@
testing.expectEqual(false, anotherDoc.contains(anotherNewNode));
testing.expectEqual(false, newDoc.contains(newNode));
</script>
<script id=detached_keeps_document>
{
const parser = new DOMParser();
const doc = parser.parseFromString('<div><p>x</p></div>', 'text/html');
const el = doc.createElement('span');
testing.expectEqual(doc, el.ownerDocument);
testing.expectEqual(doc, doc.createTextNode('x').ownerDocument);
testing.expectEqual(doc, doc.createComment('x').ownerDocument);
testing.expectEqual(doc, doc.createDocumentFragment().ownerDocument);
testing.expectEqual(doc, doc.createAttribute('a').ownerDocument);
testing.expectEqual(doc, el.cloneNode(true).ownerDocument);
// Removal does not change the node document
const div = doc.querySelector('div');
div.remove();
testing.expectEqual(doc, div.ownerDocument);
testing.expectEqual(doc, div.firstChild.ownerDocument);
// Insertion into another document adopts the whole subtree
document.body.appendChild(div);
testing.expectEqual(document, div.ownerDocument);
testing.expectEqual(document, div.firstChild.ownerDocument);
div.remove();
testing.expectEqual(document, div.ownerDocument);
// Attributes follow their element
div.setAttribute('a', '1');
testing.expectEqual(document, div.getAttributeNode('a').ownerDocument);
const attr = doc.createAttribute('b');
div.setAttributeNode(attr);
testing.expectEqual(document, attr.ownerDocument);
}
</script>
<script id=iframe_document type=module>
{
const state = await testing.async();
const f = document.createElement('iframe');
f.srcdoc = '<p>hello</p>';
f.onload = () => state.resolve(f);
document.body.appendChild(f);
await state.done((f) => {
const doc = f.contentDocument;
// Created from the parent's realm, owned by the iframe's document
const el = doc.createElement('div');
testing.expectEqual(doc, el.ownerDocument);
testing.expectEqual(doc, doc.createTextNode('x').ownerDocument);
testing.expectEqual(doc, doc.querySelector('p').cloneNode().ownerDocument);
testing.expectEqual(document, document.adoptNode(el).ownerDocument);
testing.expectEqual(doc, doc.adoptNode(el).ownerDocument);
const imported = document.importNode(doc.querySelector('p'), true);
testing.expectEqual(document, imported.ownerDocument);
testing.expectEqual(document, imported.firstChild.ownerDocument);
});
}
</script>
+3 -3
View File
@@ -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;
+8 -12
View File
@@ -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).
+2 -7
View File
@@ -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";
// <parsererror> 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;
+32 -74
View File
@@ -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, .{});
+11 -8
View File
@@ -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 {
+4 -4
View File
@@ -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 {
+13 -9
View File
@@ -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, .{});
}
}
+3 -3
View File
@@ -71,7 +71,7 @@ pub fn setBody(self: *HTMLDocument, html: []const u8, frame: *Frame) !void {
// Build a fresh <body> holding the parsed HTML as its children. Fragment
// parsing strips any <html>/<body>/<head> 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 = "",
+48 -68
View File
@@ -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 <base>; 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),
};
}
+17 -16
View File
@@ -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);
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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);
}
+9 -8
View File
@@ -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;
}
+7 -15
View File
@@ -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.
+5 -5
View File
@@ -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 <br> 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..];
}
}
+1 -1
View File
@@ -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.
+6 -1
View File
@@ -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;
+5
View File
@@ -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;
+5
View File
@@ -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();
+2 -2
View File
@@ -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 <template>, a deep clone must
@@ -173,7 +173,7 @@ pub const Build = struct {
const clone_content = clone._content.asNode();
var child_it = source._content.asNode().childrenIterator();
while (child_it.next()) |child| {
if (try child.cloneNodeForAppending(true, frame)) |cloned_child| {
if (try child.cloneNodeForAppending(true, clone_content.getDocument(frame), frame)) |cloned_child| {
try frame.appendNode(clone_content, cloned_child, .{});
}
}
+1 -1
View File
@@ -124,7 +124,7 @@ fn setDefaultValue(self: *TextArea, value: []const u8, frame: *Frame) !void {
}
// No text child exists, create one
const text_node = try Frame.node_factory.createTextNode(frame, value);
const text_node = try Frame.node_factory.createTextNode(node.getDocument(frame), value);
_ = try node.appendChild(text_node, frame);
}
+1 -1
View File
@@ -567,7 +567,7 @@ fn getResponse(self: *XMLHttpRequest, exec: *const Execution) !?Response {
if (!final.isHTML()) {
return null;
}
const document = try exec._factory.node(Node.Document{ ._proto = undefined, ._type = .generic });
const document = try exec._factory.genericDocument(.{});
try Frame.parse.htmlAsChildren(frame, document.asNode(), data);
break :blk .{ .document = document };
},