Improve usability of NodeWrapper

The NodeWrapper pattern attaches a Zig instance to a libdom Node. That works in
isolation, but for 1 given node, we might want to attach different instances.

For example, for an HTMLScriptElement we want to attach an `onError`, but for
that same node viewed as an HTMLElement we want to a `CSSStyleDeclaration`. We
can only have one. Currently, this code will crash if, for example, we create
the embedded data as an HTMLScriptElement, then try to read the embedded data
as an HTMLElement.

This PR introduces dedicated state class. So if you want the onError property,
you no longer ask the NodeWrapper for an HTMLSCriptElement. Instead, you ask
for a storage/HTMLElement.

Nothing fancy here, just memory-inefficient optional fields. If it gets out of
hand, we'll think of something more clever.
This commit is contained in:
Karl Seguin
2025-06-04 18:04:39 +08:00
parent 9efc1a1c09
commit 19df73729a
6 changed files with 99 additions and 54 deletions

View File

@@ -28,6 +28,7 @@ const NodeUnion = @import("node.zig").Union;
const collection = @import("html_collection.zig"); const collection = @import("html_collection.zig");
const css = @import("css.zig"); const css = @import("css.zig");
const State = @import("../state/Document.zig");
const Element = @import("element.zig").Element; const Element = @import("element.zig").Element;
const ElementUnion = @import("element.zig").Union; const ElementUnion = @import("element.zig").Union;
const TreeWalker = @import("tree_walker.zig").TreeWalker; const TreeWalker = @import("tree_walker.zig").TreeWalker;
@@ -42,8 +43,6 @@ pub const Document = struct {
pub const prototype = *Node; pub const prototype = *Node;
pub const subtype = .node; pub const subtype = .node;
active_element: ?*parser.Element = null,
pub fn constructor(page: *const Page) !*parser.DocumentHTML { pub fn constructor(page: *const Page) !*parser.DocumentHTML {
const doc = try parser.documentCreateDocument( const doc = try parser.documentCreateDocument(
try parser.documentHTMLGetTitle(page.window.document), try parser.documentHTMLGetTitle(page.window.document),
@@ -245,9 +244,9 @@ pub const Document = struct {
return try TreeWalker.init(root, what_to_show, filter); return try TreeWalker.init(root, what_to_show, filter);
} }
pub fn get_activeElement(doc: *parser.Document, page: *Page) !?ElementUnion { pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
const self = try page.getOrCreateNodeWrapper(Document, @ptrCast(doc)); const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self));
if (self.active_element) |ae| { if (state.active_element) |ae| {
return try Element.toInterface(ae); return try Element.toInterface(ae);
} }
@@ -255,7 +254,16 @@ pub const Document = struct {
return try Element.toInterface(@ptrCast(body)); return try Element.toInterface(@ptrCast(body));
} }
return get_documentElement(doc); return get_documentElement(self);
}
// TODO: some elements can't be focused, like if they're disabled
// but there doesn't seem to be a generic way to check this. For example
// we could look for the "disabled" attribute, but that's only meaningful
// on certain types, and libdom's vtable doesn't seem to expose this.
pub fn setFocus(self: *parser.Document, e: *parser.ElementHTML, page: *Page) !void {
const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self));
state.active_element = @ptrCast(e);
} }
}; };

View File

@@ -30,6 +30,7 @@ const NodeList = @import("../dom/nodelist.zig").NodeList;
const Location = @import("location.zig").Location; const Location = @import("location.zig").Location;
const collection = @import("../dom/html_collection.zig"); const collection = @import("../dom/html_collection.zig");
const State = @import("../state/Document.zig");
const Walker = @import("../dom/walker.zig").WalkerDepthFirst; const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
const Cookie = @import("../storage/cookie.zig").Cookie; const Cookie = @import("../storage/cookie.zig").Cookie;
@@ -39,14 +40,6 @@ pub const HTMLDocument = struct {
pub const prototype = *Document; pub const prototype = *Document;
pub const subtype = .node; pub const subtype = .node;
ready_state: ReadyState = .loading,
const ReadyState = enum {
loading,
interactive,
complete,
};
// JS funcs // JS funcs
// -------- // --------
@@ -191,9 +184,9 @@ pub const HTMLDocument = struct {
return &page.window; return &page.window;
} }
pub fn get_readyState(node: *parser.DocumentHTML, page: *Page) ![]const u8 { pub fn get_readyState(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
const self = try page.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(node)); const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self));
return @tagName(self.ready_state); return @tagName(state.ready_state);
} }
// noop legacy functions // noop legacy functions
@@ -270,9 +263,9 @@ pub const HTMLDocument = struct {
return list.items; return list.items;
} }
pub fn documentIsLoaded(html_doc: *parser.DocumentHTML, page: *Page) !void { pub fn documentIsLoaded(self: *parser.DocumentHTML, page: *Page) !void {
const self = try page.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(html_doc)); const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self));
self.ready_state = .interactive; state.ready_state = .interactive;
const evt = try parser.eventCreate(); const evt = try parser.eventCreate();
defer parser.eventDestroy(evt); defer parser.eventDestroy(evt);
@@ -282,12 +275,12 @@ pub const HTMLDocument = struct {
.source = "document", .source = "document",
}); });
try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true }); try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true });
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt); _ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, self), evt);
} }
pub fn documentIsComplete(html_doc: *parser.DocumentHTML, page: *Page) !void { pub fn documentIsComplete(self: *parser.DocumentHTML, page: *Page) !void {
const self = try page.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(html_doc)); const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self));
self.ready_state = .complete; state.ready_state = .complete;
} }
}; };

View File

@@ -25,6 +25,7 @@ const Page = @import("../page.zig").Page;
const urlStitch = @import("../../url.zig").URL.stitch; const urlStitch = @import("../../url.zig").URL.stitch;
const URL = @import("../url/url.zig").URL; const URL = @import("../url/url.zig").URL;
const Node = @import("../dom/node.zig").Node; const Node = @import("../dom/node.zig").Node;
const State = @import("../state/HTMLElement.zig");
const Element = @import("../dom/element.zig").Element; const Element = @import("../dom/element.zig").Element;
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration; const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
@@ -112,11 +113,9 @@ pub const HTMLElement = struct {
pub const prototype = *Element; pub const prototype = *Element;
pub const subtype = .node; pub const subtype = .node;
style: CSSStyleDeclaration = .empty,
pub fn get_style(e: *parser.ElementHTML, page: *Page) !*CSSStyleDeclaration { pub fn get_style(e: *parser.ElementHTML, page: *Page) !*CSSStyleDeclaration {
const self = try page.getOrCreateNodeWrapper(HTMLElement, @ptrCast(e)); const state = try page.getOrCreateNodeWrapper(State, @ptrCast(e));
return &self.style; return &state.style;
} }
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 { pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
@@ -159,16 +158,9 @@ pub const HTMLElement = struct {
return; return;
} }
const root_node = try parser.nodeGetRootNode(@ptrCast(e));
const Document = @import("../dom/document.zig").Document; const Document = @import("../dom/document.zig").Document;
const document = try page.getOrCreateNodeWrapper(Document, @ptrCast(root_node)); const root_node = try parser.nodeGetRootNode(@ptrCast(e));
try Document.setFocus(@ptrCast(root_node), e, page);
// TODO: some elements can't be focused, like if they're disabled
// but there doesn't seem to be a generic way to check this. For example
// we could look for the "disabled" attribute, but that's only meaningful
// on certain types, and libdom's vtable doesn't seem to expose this.
document.active_element = @ptrCast(e);
} }
}; };
@@ -852,9 +844,6 @@ pub const HTMLScriptElement = struct {
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const subtype = .node; pub const subtype = .node;
onload: ?Env.Function = null,
onerror: ?Env.Function = null,
pub fn get_src(self: *parser.Script) !?[]const u8 { pub fn get_src(self: *parser.Script) !?[]const u8 {
return try parser.elementGetAttribute( return try parser.elementGetAttribute(
parser.scriptToElt(self), parser.scriptToElt(self),
@@ -964,24 +953,24 @@ pub const HTMLScriptElement = struct {
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule"); return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule");
} }
pub fn get_onload(script: *parser.Script, page: *Page) !?Env.Function { pub fn get_onload(self: *parser.Script, page: *Page) !?Env.Function {
const self = page.getNodeWrapper(HTMLScriptElement, @ptrCast(script)) orelse return null; const state = page.getNodeWrapper(State, @ptrCast(self)) orelse return null;
return self.onload; return state.onload;
} }
pub fn set_onload(script: *parser.Script, function: ?Env.Function, page: *Page) !void { pub fn set_onload(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
const self = try page.getOrCreateNodeWrapper(HTMLScriptElement, @ptrCast(script)); const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self));
self.onload = function; state.onload = function;
} }
pub fn get_onerror(script: *parser.Script, page: *Page) !?Env.Function { pub fn get_onerror(self: *parser.Script, page: *Page) !?Env.Function {
const self = page.getNodeWrapper(HTMLScriptElement, @ptrCast(script)) orelse return null; const state = page.getNodeWrapper(State, @ptrCast(self)) orelse return null;
return self.onerror; return state.onerror;
} }
pub fn set_onerror(script: *parser.Script, function: ?Env.Function, page: *Page) !void { pub fn set_onerror(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
const self = try page.getOrCreateNodeWrapper(HTMLScriptElement, @ptrCast(script)); const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self));
self.onerror = function; state.onerror = function;
} }
}; };

View File

@@ -743,8 +743,8 @@ const Script = struct {
// attached to it. But this seems quite unlikely and it does help // attached to it. But this seems quite unlikely and it does help
// optimize loading scripts, of which there can be hundreds for a // optimize loading scripts, of which there can be hundreds for a
// page. // page.
const HTMLScriptElement = @import("html/elements.zig").HTMLScriptElement; const State = @import("state/HTMLElement.zig");
if (page.getNodeWrapper(HTMLScriptElement, @ptrCast(e))) |se| { if (page.getNodeWrapper(State, @ptrCast(e))) |se| {
if (se.onload) |function| { if (se.onload) |function| {
onload = .{ .function = function }; onload = .{ .function = function };
} }

View File

@@ -0,0 +1,31 @@
// Copyright (C) 2023-2024 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/>.
const parser = @import("../netsurf.zig");
// proxy-owner for html/document
ready_state: ReadyState = .loading,
// proxy-owner for dom/document
active_element: ?*parser.Element = null,
const ReadyState = enum {
loading,
interactive,
complete,
};

View File

@@ -0,0 +1,24 @@
// Copyright (C) 2023-2024 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/>.
const Env = @import("../env.zig").Env;
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
onload: ?Env.Function = null,
onerror: ?Env.Function = null,
style: CSSStyleDeclaration = .empty,