HtmlInputElement as Zig native

This commit is contained in:
sjorsdonkers
2025-06-10 17:50:39 +02:00
parent 3544e98871
commit 6ec0d0b84c
14 changed files with 117 additions and 78 deletions

View File

@@ -29,7 +29,6 @@
const Env = @import("env.zig").Env; const Env = @import("env.zig").Env;
const parser = @import("netsurf.zig"); const parser = @import("netsurf.zig");
const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration; 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) // for HTMLScript (but probably needs to be added to more)
onload: ?Env.Function = null, onload: ?Env.Function = null,
@@ -59,9 +58,6 @@ active_element: ?*parser.Element = null,
// default (by returning selectedIndex == 0). // default (by returning selectedIndex == 0).
explicit_index_set: bool = false, explicit_index_set: bool = false,
// TODO
page: ?*Page = null,
const ReadyState = enum { const ReadyState = enum {
loading, loading,
interactive, interactive,

View File

@@ -43,6 +43,7 @@ const Matcher = struct {
} }
}; };
const Elements = @import("../html/elements.zig");
test "matchFirst" { test "matchFirst" {
const alloc = std.testing.allocator; const alloc = std.testing.allocator;
@@ -161,7 +162,7 @@ test "matchFirst" {
for (testcases) |tc| { for (testcases) |tc| {
matcher.reset(); matcher.reset();
const doc = try parser.documentHTMLParseFromStr(tc.html); const doc = try parser.documentHTMLParseFromStr(tc.html, &Elements.createElement);
defer parser.documentHTMLClose(doc) catch {}; defer parser.documentHTMLClose(doc) catch {};
const s = css.parse(alloc, tc.q, .{}) catch |e| { const s = css.parse(alloc, tc.q, .{}) catch |e| {
@@ -302,7 +303,7 @@ test "matchAll" {
for (testcases) |tc| { for (testcases) |tc| {
matcher.reset(); matcher.reset();
const doc = try parser.documentHTMLParseFromStr(tc.html); const doc = try parser.documentHTMLParseFromStr(tc.html, &Elements.createElement);
defer parser.documentHTMLClose(doc) catch {}; defer parser.documentHTMLClose(doc) catch {};
const s = css.parse(alloc, tc.q, .{}) catch |e| { const s = css.parse(alloc, tc.q, .{}) catch |e| {

View File

@@ -30,6 +30,7 @@ const css = @import("css.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 Elements = @import("../html/elements.zig");
const TreeWalker = @import("tree_walker.zig").TreeWalker; const TreeWalker = @import("tree_walker.zig").TreeWalker;
const Env = @import("../env.zig").Env; const Env = @import("../env.zig").Env;
@@ -45,6 +46,7 @@ pub const Document = struct {
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),
&Elements.createElement,
); );
// we have to work w/ document instead of html document. // we have to work w/ document instead of html document.

View File

@@ -30,8 +30,8 @@ pub const DOMParser = struct {
// TODO: Support XML // TODO: Support XML
return error.TypeError; return error.TypeError;
} }
const Elements = @import("../html/elements.zig");
return try parser.documentHTMLParseFromStr(string); return try parser.documentHTMLParseFromStr(string, &Elements.createElement);
} }
}; };

View File

@@ -182,7 +182,7 @@ fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
const testing = std.testing; const testing = std.testing;
test "dump.writeHTML" { test "dump.writeHTML" {
try parser.init(); try parser.init(testing.allocator);
defer parser.deinit(); defer parser.deinit();
try testWriteHTML( try testWriteHTML(
@@ -225,7 +225,8 @@ fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
var buf = std.ArrayListUnmanaged(u8){}; var buf = std.ArrayListUnmanaged(u8){};
defer buf.deinit(testing.allocator); 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 {}; defer parser.documentHTMLClose(doc_html) catch {};
const doc = parser.documentHTMLToDocument(doc_html); const doc = parser.documentHTMLToDocument(doc_html);

View File

@@ -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 { pub fn createElement(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.?;
const p: *parser.c.dom_html_element_create_params = @ptrCast(params); const p: *parser.c.dom_html_element_create_params = @ptrCast(params);
switch (p.type) { switch (p.type) {
parser.c.DOM_HTML_ELEMENT_TYPE_INPUT => { 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 HTMLInputElement = struct {
pub const Self = parser.Input; pub const Self = parser.Input;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const subtype = .node; 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, base: parser.ElementHTML,
// Should instead have 2 vtable fields to generate the creation function type: []const u8 = "text",
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
self.dom_initialise(params); pub fn dom_create(params: *parser.c.dom_html_element_create_params, output: *?*parser.ElementHTML) parser.c.dom_exception {
return self.base; 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 // 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 { 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); 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 // 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 { pub fn node_destroy(node: [*c]parser.Node) callconv(.c) void {
// parser._dom_html_element_finalise(@as(parser.HtmlElement, @ptrCast(&self.base))); const elem = parser.nodeToHtmlElement(node);
// } parser.c._dom_html_element_finalise(elem);
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 dom_element_parse_attribute(self: *parser.Element, name: []const u8, value: []const u8, page: *Page) ![]const u8 { pub fn node_copy(old: [*c]parser.Node, new: [*c][*c]parser.Node) callconv(.c) parser.c.dom_exception {
// _ = page; const old_elem = parser.nodeToHtmlElement(old);
// _ = name; const self = @as(*HTMLInputElement, @fieldParentPtr("base", old_elem));
// _ = self;
// // Probably should not use this and instead override the getAttribute setAttribute Element methods directly, perhaps other related functions.
// // handle defaultValue likes var copy = parser.ARENA.?.create(HTMLInputElement) catch return parser.c.DOM_NO_MEM_ERR;
// // Call setter or store in general attribute store copy.type = self.type;
// // increment domstring ref?
// return value; const err = parser.c._dom_html_element_copy_internal(old_elem, &copy.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 { pub fn get_defaultValue(self: *parser.Input) ![]const u8 {
return try parser.inputGetDefaultValue(self); return try parser.inputGetDefaultValue(self);
@@ -768,10 +777,26 @@ pub const HTMLInputElement = struct {
try parser.inputSetSrc(self, new_src); try parser.inputSetSrc(self, new_src);
} }
pub fn get_type(self: *parser.Input) ![]const u8 { 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 { 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 { pub fn get_value(self: *parser.Input) ![]const u8 {
return try parser.inputGetValue(self); return try parser.inputGetValue(self);

View File

@@ -61,7 +61,8 @@ pub const Window = struct {
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window { pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
var fbs = std.io.fixedBufferStream(""); 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); const doc = parser.documentHTMLToDocument(html_doc);
try parser.documentSetDocumentURI(doc, "about:blank"); try parser.documentSetDocumentURI(doc, "about:blank");

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
pub const c = @cImport({ pub const c = @cImport({
@cInclude("dom/dom.h"); @cInclude("dom/dom.h");
@@ -32,11 +33,13 @@ pub const c = @cImport({
}); });
const mimalloc = @import("mimalloc.zig"); const mimalloc = @import("mimalloc.zig");
pub var ARENA: ?Allocator = null;
// init initializes netsurf lib. // init initializes netsurf lib.
// init starts a mimalloc heap arena for the netsurf session. The caller must // init starts a mimalloc heap arena for the netsurf session. The caller must
// call deinit() to free the arena memory. // call deinit() to free the arena memory.
pub fn init() !void { pub fn init(allocator: Allocator) !void {
ARENA = allocator;
try mimalloc.create(); try mimalloc.create();
} }
@@ -49,6 +52,7 @@ pub fn deinit() void {
c.lwc_deinit_strings(); c.lwc_deinit_strings();
mimalloc.destroy(); mimalloc.destroy();
ARENA = null;
} }
// Vtable // Vtable
@@ -1357,6 +1361,10 @@ pub inline fn nodeToElement(node: *Node) *Element {
return @as(*Element, @ptrCast(node)); 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. // nodeToDocument is an helper to convert a node to an document.
pub inline fn nodeToDocument(node: *Node) *Document { pub inline fn nodeToDocument(node: *Node) *Document {
return @as(*Document, @ptrCast(node)); return @as(*Document, @ptrCast(node));
@@ -1989,10 +1997,10 @@ pub inline fn domImplementationCreateDocumentType(
return dt.?; 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 { 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); const doc = documentHTMLToDocument(doc_html);
// add hierarchy: html, head, body. // add hierarchy: html, head, body.
@@ -2012,7 +2020,6 @@ pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8, create_ele
const body = try documentCreateElement(doc, "body"); const body = try documentCreateElement(doc, "body");
_ = try nodeAppendChild(elementToNode(html), elementToNode(body)); _ = try nodeAppendChild(elementToNode(html), elementToNode(body));
doc_html.create_element_external = create_element;
return doc_html; return doc_html;
} }
@@ -2074,7 +2081,7 @@ pub inline fn documentSetInputEncoding(doc: *Document, enc: []const u8) !void {
try DOMErr(err); 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; var doc: ?*Document = undefined;
const err = c.dom_implementation_create_document( const err = c.dom_implementation_create_document(
c.DOM_IMPLEMENTATION_HTML, c.DOM_IMPLEMENTATION_HTML,
@@ -2089,7 +2096,7 @@ pub inline fn documentCreateDocument(title: ?[]const u8) !*DocumentHTML {
const doc_html = @as(*DocumentHTML, @ptrCast(doc.?)); const doc_html = @as(*DocumentHTML, @ptrCast(doc.?));
if (title) |t| try documentHTMLSetTitle(doc_html, t); if (title) |t| try documentHTMLSetTitle(doc_html, t);
// doc_html.create_element_external = doc_html.create_element_external = create_element;
return doc_html; return doc_html;
} }
@@ -2254,24 +2261,26 @@ fn parserErr(err: HubbubErr) ParserError!void {
// documentHTMLParseFromStr parses the given HTML string. // documentHTMLParseFromStr parses the given HTML string.
// The caller is responsible for closing the document. // 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); 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 parser: ?*c.dom_hubbub_parser = undefined;
var doc: ?*c.dom_document = undefined; var doc: ?*c.dom_document = undefined;
var err: c.hubbub_error = undefined; var err: c.hubbub_error = undefined;
var params = parseParams(enc); var params = parseParams(enc);
err = c.dom_hubbub_parser_create(&params, &parser, &doc); err = c.dom_hubbub_parser_create(&params, &parser, &doc);
const result = @as(*DocumentHTML, @ptrCast(doc.?));
result.create_element_external = create_element;
try parserErr(err); try parserErr(err);
defer c.dom_hubbub_parser_destroy(parser); defer c.dom_hubbub_parser_destroy(parser);
try parseData(parser.?, reader); try parseData(parser.?, reader);
return @as(*DocumentHTML, @ptrCast(doc.?)); return result;
} }
pub fn documentParseFragmentFromStr(self: *Document, str: []const u8) !*DocumentFragment { pub fn documentParseFragmentFromStr(self: *Document, str: []const u8) !*DocumentFragment {

View File

@@ -286,7 +286,8 @@ pub const Page = struct {
pub fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void { pub fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
const ccharset = try self.arena.dupeZ(u8, charset); 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); const doc = parser.documentHTMLToDocument(html_doc);
// inject the URL to the document including the fragment. // inject the URL to the document including the fragment.

View File

@@ -85,14 +85,14 @@ pub const Session = struct {
pub fn createPage(self: *Session) !*Page { pub fn createPage(self: *Session) !*Page {
std.debug.assert(self.page == null); 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; const page_arena = &self.browser.page_arena;
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); _ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
_ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 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); self.page = @as(Page, undefined);
const page = &self.page.?; const page = &self.page.?;
try Page.init(page, page_arena.allocator(), self); try Page.init(page, page_arena.allocator(), self);

View File

@@ -756,7 +756,8 @@ pub const XMLHttpRequest = struct {
} }
var fbs = std.io.fixedBufferStream(self.response_bytes.items); 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 = {} }; self.response_obj = .{ .Failure = {} };
return; return;
}; };

View File

@@ -522,7 +522,7 @@ test {
var test_wg: std.Thread.WaitGroup = .{}; var test_wg: std.Thread.WaitGroup = .{};
test "tests:beforeAll" { test "tests:beforeAll" {
try parser.init(); try parser.init(std.testing.allocator);
log.opts.level = .err; log.opts.level = .err;
log.opts.format = .logfmt; log.opts.format = .logfmt;

View File

@@ -211,14 +211,16 @@ pub const Document = struct {
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
pub fn init(html: []const u8) !Document { pub fn init(html: []const u8) !Document {
var arena = std.heap.ArenaAllocator.init(allocator);
parser.deinit(); parser.deinit();
try parser.init(); try parser.init(arena.allocator());
var fbs = std.io.fixedBufferStream(html); 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 .{ return .{
.arena = std.heap.ArenaAllocator.init(allocator), .arena = arena,
.doc = html_doc, .doc = html_doc,
}; };
} }