From 6ec0d0b84cac2d322b6a9c0a8d0407e0ac667fe5 Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:50:39 +0200 Subject: [PATCH] HtmlInputElement as Zig native --- src/browser/State.zig | 4 -- src/browser/css/libdom_test.zig | 5 +- src/browser/dom/document.zig | 2 + src/browser/dom/dom_parser.zig | 4 +- src/browser/dump.zig | 5 +- src/browser/html/elements.zig | 117 +++++++++++++++++++------------- src/browser/html/window.zig | 3 +- src/browser/netsurf.zig | 29 +++++--- src/browser/page.zig | 3 +- src/browser/session.zig | 8 +-- src/browser/xhr/xhr.zig | 3 +- src/main.zig | 2 +- src/testing.zig | 8 ++- vendor/netsurf/libdom | 2 +- 14 files changed, 117 insertions(+), 78 deletions(-) diff --git a/src/browser/State.zig b/src/browser/State.zig index 9f02d4e4..55447923 100644 --- a/src/browser/State.zig +++ b/src/browser/State.zig @@ -29,7 +29,6 @@ const Env = @import("env.zig").Env; const parser = @import("netsurf.zig"); const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration; -const Page = @import("page.zig").Page; // for HTMLScript (but probably needs to be added to more) onload: ?Env.Function = null, @@ -59,9 +58,6 @@ active_element: ?*parser.Element = null, // default (by returning selectedIndex == 0). explicit_index_set: bool = false, -// TODO -page: ?*Page = null, - const ReadyState = enum { loading, interactive, diff --git a/src/browser/css/libdom_test.zig b/src/browser/css/libdom_test.zig index 4cd267e0..74e9ecdb 100644 --- a/src/browser/css/libdom_test.zig +++ b/src/browser/css/libdom_test.zig @@ -43,6 +43,7 @@ const Matcher = struct { } }; +const Elements = @import("../html/elements.zig"); test "matchFirst" { const alloc = std.testing.allocator; @@ -161,7 +162,7 @@ test "matchFirst" { for (testcases) |tc| { matcher.reset(); - const doc = try parser.documentHTMLParseFromStr(tc.html); + const doc = try parser.documentHTMLParseFromStr(tc.html, &Elements.createElement); defer parser.documentHTMLClose(doc) catch {}; const s = css.parse(alloc, tc.q, .{}) catch |e| { @@ -302,7 +303,7 @@ test "matchAll" { for (testcases) |tc| { matcher.reset(); - const doc = try parser.documentHTMLParseFromStr(tc.html); + const doc = try parser.documentHTMLParseFromStr(tc.html, &Elements.createElement); defer parser.documentHTMLClose(doc) catch {}; const s = css.parse(alloc, tc.q, .{}) catch |e| { diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index bc8dccbc..9e7c7426 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -30,6 +30,7 @@ const css = @import("css.zig"); const Element = @import("element.zig").Element; const ElementUnion = @import("element.zig").Union; +const Elements = @import("../html/elements.zig"); const TreeWalker = @import("tree_walker.zig").TreeWalker; const Env = @import("../env.zig").Env; @@ -45,6 +46,7 @@ pub const Document = struct { pub fn constructor(page: *const Page) !*parser.DocumentHTML { const doc = try parser.documentCreateDocument( try parser.documentHTMLGetTitle(page.window.document), + &Elements.createElement, ); // we have to work w/ document instead of html document. diff --git a/src/browser/dom/dom_parser.zig b/src/browser/dom/dom_parser.zig index 505d2b10..1f276328 100644 --- a/src/browser/dom/dom_parser.zig +++ b/src/browser/dom/dom_parser.zig @@ -30,8 +30,8 @@ pub const DOMParser = struct { // TODO: Support XML return error.TypeError; } - - return try parser.documentHTMLParseFromStr(string); + const Elements = @import("../html/elements.zig"); + return try parser.documentHTMLParseFromStr(string, &Elements.createElement); } }; diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 35eb4d42..b670b285 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -182,7 +182,7 @@ fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void { const testing = std.testing; test "dump.writeHTML" { - try parser.init(); + try parser.init(testing.allocator); defer parser.deinit(); try testWriteHTML( @@ -225,7 +225,8 @@ fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void { var buf = std.ArrayListUnmanaged(u8){}; defer buf.deinit(testing.allocator); - const doc_html = try parser.documentHTMLParseFromStr(src); + const Elements = @import("html/elements.zig"); + const doc_html = try parser.documentHTMLParseFromStr(src, &Elements.createElement); defer parser.documentHTMLClose(doc_html) catch {}; const doc = parser.documentHTMLToDocument(doc_html); diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 4f88e1fb..f96cfe5f 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -627,75 +627,84 @@ pub const HTMLImageElement = struct { }; }; -pub fn createElement(doc: [*c]parser.DocumentHTML, params: [*c]parser.c.dom_html_element_create_params, elem: [*c][*c]parser.ElementHTML) callconv(.c) parser.c.dom_exception { - // Required to be set on all htmldocuments. How during dom parsing? - const wrap = parser.nodeGetEmbedderData(@ptrCast(doc)).?; // TODO this is not set yet - const state = @as(*State, @alignCast(@ptrCast(wrap))); - const page = state.page.?; - +pub fn createElement(params: [*c]parser.c.dom_html_element_create_params, elem: [*c][*c]parser.ElementHTML) callconv(.c) parser.c.dom_exception { const p: *parser.c.dom_html_element_create_params = @ptrCast(params); switch (p.type) { parser.c.DOM_HTML_ELEMENT_TYPE_INPUT => { - elem.* = try HTMLInputElement.dom_create(params, page); + return HTMLInputElement.dom_create(params, elem); }, - else => return parser.c.DOM_NOT_FOUND_ERR, + else => return parser.c.DOM_NO_ERR, } - return parser.c.DOM_NO_ERR; } +var input_protected_vtable: parser.c.dom_element_protected_vtable = .{ + .base = .{ + .destroy = HTMLInputElement.node_destroy, + .copy = HTMLInputElement.node_copy, + }, + .dom_element_parse_attribute = HTMLInputElement.element_parse_attribute, +}; + pub const HTMLInputElement = struct { pub const Self = parser.Input; pub const prototype = *HTMLElement; pub const subtype = .node; - // VTables can be generated from the dom_ funcs - vtable: parser.c.dom_html_element_vtable = parser.c._dom_html_element_vtable, // TODO make global, instantiate value and cast to void probably - protected_vtable: parser.c.dom_element_protected_vtable = .{ - .dom_node_copy = dom_node_copy, - // .dom_node_destroy = dom_node_destroy, // Not needed in zig - .dom_initialise = dom_initialise, - }, - base: parser.ElementHTML, - // Should instead have 2 vtable fields to generate the creation function - pub fn dom_create(params: *parser.c.dom_html_element_create_params, page: *Page) !*parser.ElementHTML { - var self = try page.arena.create(HTMLInputElement); // Put in pool? - self.base.base.base.vtable = &HTMLInputElement.vtable; - self.base.base.vtable = &HTMLInputElement.protected_vtable; - // set vtable and protected vtable + type: []const u8 = "text", - self.dom_initialise(params); - return self.base; + pub fn dom_create(params: *parser.c.dom_html_element_create_params, output: *?*parser.ElementHTML) parser.c.dom_exception { + var self = parser.ARENA.?.create(HTMLInputElement) catch return parser.c.DOM_NO_MEM_ERR; + output.* = &self.base; // Self can be recovered using @fieldParentPtr + + self.base.base.base.base.vtable = &parser.c._dom_html_element_vtable; // TODO replace get/setAttribute + self.base.base.base.vtable = &input_protected_vtable; + + return self.dom_initialise(params); } // Initialise is separated from create such that the leaf type sets the vtable, then calls all the way up the protochain to init - // Currently we do only leaf types tho pub fn dom_initialise(self: *HTMLInputElement, params: *parser.c.dom_html_element_create_params) parser.c.dom_exception { return parser.c._dom_html_element_initialise(params, &self.base); } // This should always be the same and we should not have cleanup for new zig implementation, hopefully - // pub fn dom_node_destroy(self: *parser.Node) !void { - // parser._dom_html_element_finalise(@as(parser.HtmlElement, @ptrCast(&self.base))); - // } - - pub fn dom_node_copy(old: *parser.Node, page: Page) !*parser.Node { - const self = @as(*HTMLInputElement, @fieldParentPtr("base", old)); - const copy = try HTMLInputElement.create(&self.base.create_params, page); - return @ptrCast(copy); + pub fn node_destroy(node: [*c]parser.Node) callconv(.c) void { + const elem = parser.nodeToHtmlElement(node); + parser.c._dom_html_element_finalise(elem); } - // pub fn dom_element_parse_attribute(self: *parser.Element, name: []const u8, value: []const u8, page: *Page) ![]const u8 { - // _ = page; - // _ = name; - // _ = self; - // // Probably should not use this and instead override the getAttribute setAttribute Element methods directly, perhaps other related functions. + pub fn node_copy(old: [*c]parser.Node, new: [*c][*c]parser.Node) callconv(.c) parser.c.dom_exception { + const old_elem = parser.nodeToHtmlElement(old); + const self = @as(*HTMLInputElement, @fieldParentPtr("base", old_elem)); - // // handle defaultValue likes - // // Call setter or store in general attribute store - // // increment domstring ref? - // return value; - // } + var copy = parser.ARENA.?.create(HTMLInputElement) catch return parser.c.DOM_NO_MEM_ERR; + copy.type = self.type; + + const err = parser.c._dom_html_element_copy_internal(old_elem, ©.base); + if (err != parser.c.DOM_NO_ERR) { + return err; + } + + new.* = @ptrCast(copy); + return parser.c.DOM_NO_ERR; + } + + // fn ([*c]cimport.struct_dom_element, [*c]cimport.struct_dom_string, [*c]cimport.struct_dom_string, [*c][*c]cimport.struct_dom_string) callconv(.c) c_uint + pub fn element_parse_attribute(self: [*c]parser.Element, name: [*c]parser.c.dom_string, value: [*c]parser.c.dom_string, parsed: [*c][*c]parser.c.dom_string) callconv(.c) parser.c.dom_exception { + _ = name; + _ = self; + parsed.* = value; + _ = parser.c.dom_string_ref(value); + + // TODO actual implementation + // Probably should not use this and instead override the getAttribute setAttribute Element methods directly, perhaps other related functions. + + // handle defaultValue likes + // Call setter or store in general attribute store + // increment domstring ref? + return parser.c.DOM_NO_ERR; + } pub fn get_defaultValue(self: *parser.Input) ![]const u8 { return try parser.inputGetDefaultValue(self); @@ -768,10 +777,26 @@ pub const HTMLInputElement = struct { try parser.inputSetSrc(self, new_src); } pub fn get_type(self: *parser.Input) ![]const u8 { - return try parser.inputGetType(self); + const elem = parser.nodeToHtmlElement(@alignCast(@ptrCast(self))); + const input = @as(*HTMLInputElement, @fieldParentPtr("base", elem)); + + return input.type; } pub fn set_type(self: *parser.Input, type_: []const u8) !void { - try parser.inputSetType(self, type_); + const elem = parser.nodeToHtmlElement(@alignCast(@ptrCast(self))); + const input = @as(*HTMLInputElement, @fieldParentPtr("base", elem)); + + const possible_values = [_][]const u8{ "text", "search", "tel", "url", "email", "password", "date", "month", "week", "time", "datetime-local", "number", "range", "color", "checkbox", "radio", "file", "hidden", "image", "button", "submit", "reset" }; + var found = false; + for (possible_values) |item| { + if (std.mem.eql(u8, type_, item)) { + found = true; + break; + } + } + input.type = if (found) type_ else "text"; + + // TODO DOM events } pub fn get_value(self: *parser.Input) ![]const u8 { return try parser.inputGetValue(self); diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index 02381971..7e473ae9 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -61,7 +61,8 @@ pub const Window = struct { pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window { var fbs = std.io.fixedBufferStream(""); - const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8"); + const Elements = @import("../html/elements.zig"); + const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8", &Elements.createElement); const doc = parser.documentHTMLToDocument(html_doc); try parser.documentSetDocumentURI(doc, "about:blank"); diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index e480074e..c7e8cb72 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const Allocator = std.mem.Allocator; pub const c = @cImport({ @cInclude("dom/dom.h"); @@ -32,11 +33,13 @@ pub const c = @cImport({ }); const mimalloc = @import("mimalloc.zig"); +pub var ARENA: ?Allocator = null; // init initializes netsurf lib. // init starts a mimalloc heap arena for the netsurf session. The caller must // call deinit() to free the arena memory. -pub fn init() !void { +pub fn init(allocator: Allocator) !void { + ARENA = allocator; try mimalloc.create(); } @@ -49,6 +52,7 @@ pub fn deinit() void { c.lwc_deinit_strings(); mimalloc.destroy(); + ARENA = null; } // Vtable @@ -1357,6 +1361,10 @@ pub inline fn nodeToElement(node: *Node) *Element { return @as(*Element, @ptrCast(node)); } +pub inline fn nodeToHtmlElement(node: *Node) *ElementHTML { + return @as(*ElementHTML, @alignCast(@ptrCast(node))); +} + // nodeToDocument is an helper to convert a node to an document. pub inline fn nodeToDocument(node: *Node) *Document { return @as(*Document, @ptrCast(node)); @@ -1989,10 +1997,10 @@ pub inline fn domImplementationCreateDocumentType( return dt.?; } -pub const CreateElementFn = ?*const fn ([*c]DocumentHTML, [*c]c.dom_html_element_create_params, [*c][*c]ElementHTML) callconv(.c) c.dom_exception; +pub const CreateElementFn = ?*const fn ([*c]c.dom_html_element_create_params, [*c][*c]ElementHTML) callconv(.c) c.dom_exception; pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8, create_element: CreateElementFn) !*DocumentHTML { - const doc_html = try documentCreateDocument(title); + const doc_html = try documentCreateDocument(title, create_element); const doc = documentHTMLToDocument(doc_html); // add hierarchy: html, head, body. @@ -2012,7 +2020,6 @@ pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8, create_ele const body = try documentCreateElement(doc, "body"); _ = try nodeAppendChild(elementToNode(html), elementToNode(body)); - doc_html.create_element_external = create_element; return doc_html; } @@ -2074,7 +2081,7 @@ pub inline fn documentSetInputEncoding(doc: *Document, enc: []const u8) !void { try DOMErr(err); } -pub inline fn documentCreateDocument(title: ?[]const u8) !*DocumentHTML { +pub inline fn documentCreateDocument(title: ?[]const u8, create_element: CreateElementFn) !*DocumentHTML { var doc: ?*Document = undefined; const err = c.dom_implementation_create_document( c.DOM_IMPLEMENTATION_HTML, @@ -2089,7 +2096,7 @@ pub inline fn documentCreateDocument(title: ?[]const u8) !*DocumentHTML { const doc_html = @as(*DocumentHTML, @ptrCast(doc.?)); if (title) |t| try documentHTMLSetTitle(doc_html, t); - // doc_html.create_element_external = + doc_html.create_element_external = create_element; return doc_html; } @@ -2254,24 +2261,26 @@ fn parserErr(err: HubbubErr) ParserError!void { // documentHTMLParseFromStr parses the given HTML string. // The caller is responsible for closing the document. -pub fn documentHTMLParseFromStr(str: []const u8) !*DocumentHTML { +pub fn documentHTMLParseFromStr(str: []const u8, create_element: CreateElementFn) !*DocumentHTML { var fbs = std.io.fixedBufferStream(str); - return try documentHTMLParse(fbs.reader(), "UTF-8"); + return try documentHTMLParse(fbs.reader(), "UTF-8", create_element); } -pub fn documentHTMLParse(reader: anytype, enc: ?[:0]const u8) !*DocumentHTML { +pub fn documentHTMLParse(reader: anytype, enc: ?[:0]const u8, create_element: CreateElementFn) !*DocumentHTML { var parser: ?*c.dom_hubbub_parser = undefined; var doc: ?*c.dom_document = undefined; var err: c.hubbub_error = undefined; var params = parseParams(enc); err = c.dom_hubbub_parser_create(¶ms, &parser, &doc); + const result = @as(*DocumentHTML, @ptrCast(doc.?)); + result.create_element_external = create_element; try parserErr(err); defer c.dom_hubbub_parser_destroy(parser); try parseData(parser.?, reader); - return @as(*DocumentHTML, @ptrCast(doc.?)); + return result; } pub fn documentParseFragmentFromStr(self: *Document, str: []const u8) !*DocumentFragment { diff --git a/src/browser/page.zig b/src/browser/page.zig index 396022db..807470a1 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -286,7 +286,8 @@ pub const Page = struct { pub fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void { const ccharset = try self.arena.dupeZ(u8, charset); - const html_doc = try parser.documentHTMLParse(reader, ccharset); + const Elements = @import("html/elements.zig"); + const html_doc = try parser.documentHTMLParse(reader, ccharset, &Elements.createElement); const doc = parser.documentHTMLToDocument(html_doc); // inject the URL to the document including the fragment. diff --git a/src/browser/session.zig b/src/browser/session.zig index 8cedf7d7..b8feb957 100644 --- a/src/browser/session.zig +++ b/src/browser/session.zig @@ -85,14 +85,14 @@ pub const Session = struct { pub fn createPage(self: *Session) !*Page { std.debug.assert(self.page == null); - // Start netsurf memory arena. - // We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded - try parser.init(); - const page_arena = &self.browser.page_arena; _ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); _ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 1024 }); + // Start netsurf memory arena. + // We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded + try parser.init(page_arena.allocator()); + self.page = @as(Page, undefined); const page = &self.page.?; try Page.init(page, page_arena.allocator(), self); diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig index 37f87bee..5feda68d 100644 --- a/src/browser/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -756,7 +756,8 @@ pub const XMLHttpRequest = struct { } var fbs = std.io.fixedBufferStream(self.response_bytes.items); - const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch { + const Elements = @import("../html/elements.zig"); + const doc = parser.documentHTMLParse(fbs.reader(), ccharset, &Elements.createElement) catch { self.response_obj = .{ .Failure = {} }; return; }; diff --git a/src/main.zig b/src/main.zig index c95f7a6d..c4a728a5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -522,7 +522,7 @@ test { var test_wg: std.Thread.WaitGroup = .{}; test "tests:beforeAll" { - try parser.init(); + try parser.init(std.testing.allocator); log.opts.level = .err; log.opts.format = .logfmt; diff --git a/src/testing.zig b/src/testing.zig index 75bb3a95..39e49d52 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -211,14 +211,16 @@ pub const Document = struct { arena: std.heap.ArenaAllocator, pub fn init(html: []const u8) !Document { + var arena = std.heap.ArenaAllocator.init(allocator); parser.deinit(); - try parser.init(); + try parser.init(arena.allocator()); var fbs = std.io.fixedBufferStream(html); - const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8"); + const Elements = @import("browser/html/elements.zig"); + const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8", &Elements.createElement); return .{ - .arena = std.heap.ArenaAllocator.init(allocator), + .arena = arena, .doc = html_doc, }; } diff --git a/vendor/netsurf/libdom b/vendor/netsurf/libdom index af1b1757..f22449c5 160000 --- a/vendor/netsurf/libdom +++ b/vendor/netsurf/libdom @@ -1 +1 @@ -Subproject commit af1b1757ad9b1bbdb1eff8d8420bce1c167b8fcc +Subproject commit f22449c52e236a315337c2a213fbcf0b1280aa15