From 747a8ad09c41c501cfb8855eb07a4250f69251df Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 2 Jun 2025 11:27:44 +0800 Subject: [PATCH 1/2] Submit input and button submits can now submit forms --- src/browser/html/form.zig | 4 ---- src/browser/page.zig | 43 +++++++++++++++++++++++++++++++++++ src/browser/xhr/form_data.zig | 14 ++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/browser/html/form.zig b/src/browser/html/form.zig index cd90589c..13443667 100644 --- a/src/browser/html/form.zig +++ b/src/browser/html/form.zig @@ -32,10 +32,6 @@ pub const HTMLFormElement = struct { return page.submitForm(self, null); } - pub fn _requestSubmit(self: *parser.Form) !void { - try parser.formElementSubmit(self); - } - pub fn _reset(self: *parser.Form) !void { try parser.formElementReset(self); } diff --git a/src/browser/page.zig b/src/browser/page.zig index e8f29110..ae313eaa 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -553,6 +553,25 @@ pub const Page = struct { const href = (try parser.elementGetAttribute(element, "href")) orelse return; try self.navigateFromWebAPI(href, .{}); }, + .input => { + const element: *parser.Element = @ptrCast(node); + const input_type = (try parser.elementGetAttribute(element, "type")) orelse return; + if (std.ascii.eqlIgnoreCase(input_type, "submit")) { + return self.elementSubmitForm(element); + } + }, + .button => { + const element: *parser.Element = @ptrCast(node); + const button_type = (try parser.elementGetAttribute(element, "type")) orelse return; + if (std.ascii.eqlIgnoreCase(button_type, "submit")) { + return self.elementSubmitForm(element); + } + if (std.ascii.eqlIgnoreCase(button_type, "reset")) { + if (try self.formForElement(element)) |form| { + return parser.formElementReset(form); + } + } + }, else => {}, } } @@ -616,6 +635,30 @@ pub const Page = struct { try self.navigateFromWebAPI(action, opts); } + + fn elementSubmitForm(self: *Page, element: *parser.Element) !void { + const form = (try self.formForElement(element)) orelse return; + return self.submitForm(@ptrCast(form), @ptrCast(element)); + } + + fn formForElement(self: *Page, element: *parser.Element) !?*parser.Form { + if (try parser.elementGetAttribute(element, "disabled") != null) { + return null; + } + + if (try parser.elementGetAttribute(element, "form")) |form_id| { + const document = parser.documentHTMLToDocument(self.window.document); + const form_element = try parser.documentGetElementById(document, form_id) orelse return null; + if (try parser.elementHTMLGetTagType(@ptrCast(form_element)) == .form) { + return @ptrCast(form_element); + } + return null; + } + + const Element = @import("dom/element.zig").Element; + const form = (try Element._closest(element, "form", self)) orelse return null; + return @ptrCast(form); + } }; const DelayedNavigation = struct { diff --git a/src/browser/xhr/form_data.zig b/src/browser/xhr/form_data.zig index 32d56bc9..ed9e1ebe 100644 --- a/src/browser/xhr/form_data.zig +++ b/src/browser/xhr/form_data.zig @@ -268,6 +268,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page var entries: Entry.List = .empty; try entries.ensureTotalCapacity(arena, len); + var submitter_included = false; const submitter_name_ = try getSubmitterName(submitter_); for (0..len) |i| { @@ -295,6 +296,8 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page .key = try std.fmt.allocPrint(arena, "{s}.y", .{name}), .value = "0", }); + + submitter_included = true; } } continue; @@ -309,6 +312,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page if (submitter_name_ == null or !std.mem.eql(u8, submitter_name_.?, name)) { continue; } + submitter_included = true; } const value = (try parser.elementGetAttribute(element, "value")) orelse ""; try entries.append(arena, .{ .key = name, .value = value }); @@ -326,6 +330,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page if (std.mem.eql(u8, submitter_name, name)) { const value = (try parser.elementGetAttribute(element, "value")) orelse ""; try entries.append(arena, .{ .key = name, .value = value }); + submitter_included = true; } }, else => { @@ -335,6 +340,15 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page } } + if (submitter_included == false) { + if (submitter_) |submitter| { + // this can happen if the submitter is outside the form, but associated + // with the form via a form=ID attribute + const value = (try parser.elementGetAttribute(@ptrCast(submitter), "value")) orelse ""; + try entries.append(arena, .{ .key = submitter_name_.?, .value = value }); + } + } + return entries; } From 4644e55883db619fa67a83c6d42a44f6b1a102e1 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 2 Jun 2025 14:16:36 +0800 Subject: [PATCH 2/2] Do not reset transfer_arena if page navigation results in delayed navigation We normally expect a navigation event to happen at some point after the page loads, like a puppeteer script clicking on a link. But, it's also possible for the main navigation event to result in a delayed navigation. For example, an html page with this JS: Would result in a delayed navigation being called from the main navigate function. In these cases, we cannot clear the transfer_arena when navigate is completed, as its memory is needed by the new "sub" delayed navigation. --- src/browser/page.zig | 4 ++++ src/browser/session.zig | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/browser/page.zig b/src/browser/page.zig index ae313eaa..ebb90528 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -92,6 +92,9 @@ pub const Page = struct { // current_script could by fetch module to resolve module's url to fetch. current_script: ?*const Script = null, + // indicates intention to navigate to another page on the next loop execution. + delayed_navigation: bool = false, + pub fn init(self: *Page, arena: Allocator, session: *Session) !void { const browser = session.browser; self.* = .{ @@ -580,6 +583,7 @@ pub const Page = struct { // The page.arena is safe to use here, but the transfer_arena exists // specifically for this type of lifetime. pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts) !void { + self.delayed_navigation = true; const arena = self.session.transfer_arena; const navi = try arena.create(DelayedNavigation); navi.* = .{ diff --git a/src/browser/session.zig b/src/browser/session.zig index d71bb27e..532a3907 100644 --- a/src/browser/session.zig +++ b/src/browser/session.zig @@ -128,7 +128,14 @@ pub const Session = struct { // it isn't null! std.debug.assert(self.page != null); - defer _ = self.browser.transfer_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); + defer if (self.page) |*p| { + if (!p.delayed_navigation) { + // If, while loading the page, we intend to navigate to another + // page, then we need to keep the transfer_arena around, as this + // sub-navigation is probably using it. + _ = self.browser.transfer_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); + } + }; // it's safe to use the transfer arena here, because the page will // eventually clone the URL using its own page_arena (after it gets