diff --git a/src/browser/tests/element/html/form.html b/src/browser/tests/element/html/form.html
new file mode 100644
index 00000000..9c5055e3
--- /dev/null
+++ b/src/browser/tests/element/html/form.html
@@ -0,0 +1,299 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/legacy/xhr/form_data.html b/src/browser/tests/legacy/xhr/form_data.html
index 94bf8a27..cda34c06 100644
--- a/src/browser/tests/legacy/xhr/form_data.html
+++ b/src/browser/tests/legacy/xhr/form_data.html
@@ -1,5 +1,42 @@
+
+
+
+
-
-
diff --git a/src/browser/tests/net/form_data.html b/src/browser/tests/net/form_data.html
index 71aae77c..515b8d93 100644
--- a/src/browser/tests/net/form_data.html
+++ b/src/browser/tests/net/form_data.html
@@ -250,3 +250,134 @@
testing.expectEqual(3, context.sum);
}
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig
index 532a335c..e27ca505 100644
--- a/src/browser/webapi/Node.zig
+++ b/src/browser/webapi/Node.zig
@@ -339,7 +339,7 @@ pub fn isConnected(self: *const Node) bool {
const GetRootNodeOpts = struct {
composed: bool = false,
};
-pub fn getRootNode(self: *const Node, opts_: ?GetRootNodeOpts) *const Node {
+pub fn getRootNode(self: *Node, opts_: ?GetRootNodeOpts) *Node {
const opts = opts_ orelse GetRootNodeOpts{};
var root = self;
@@ -613,7 +613,7 @@ pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) error{ OutOfMemory, Str
}
}
-pub fn compareDocumentPosition(self: *const Node, other: *const Node) u16 {
+pub fn compareDocumentPosition(self: *Node, other: *Node) u16 {
const DISCONNECTED: u16 = 0x01;
const PRECEDING: u16 = 0x02;
const FOLLOWING: u16 = 0x04;
diff --git a/src/browser/webapi/collections.zig b/src/browser/webapi/collections.zig
index 3e36e05f..d0dd81ce 100644
--- a/src/browser/webapi/collections.zig
+++ b/src/browser/webapi/collections.zig
@@ -21,6 +21,7 @@ pub const ChildNodes = @import("collections/ChildNodes.zig");
pub const DOMTokenList = @import("collections/DOMTokenList.zig");
pub const HTMLAllCollection = @import("collections/HTMLAllCollection.zig");
pub const HTMLOptionsCollection = @import("collections/HTMLOptionsCollection.zig");
+pub const HTMLFormControlsCollection = @import("collections/HTMLFormControlsCollection.zig");
pub fn registerTypes() []const type {
return &.{
@@ -33,6 +34,7 @@ pub fn registerTypes() []const type {
@import("collections/HTMLAllCollection.zig"),
@import("collections/HTMLAllCollection.zig").Iterator,
HTMLOptionsCollection,
+ HTMLFormControlsCollection,
DOMTokenList,
DOMTokenList.KeyIterator,
DOMTokenList.ValueIterator,
diff --git a/src/browser/webapi/collections/HTMLCollection.zig b/src/browser/webapi/collections/HTMLCollection.zig
index 3160524d..462f1b37 100644
--- a/src/browser/webapi/collections/HTMLCollection.zig
+++ b/src/browser/webapi/collections/HTMLCollection.zig
@@ -23,6 +23,7 @@ const Page = @import("../../Page.zig");
const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig");
const NodeLive = @import("node_live.zig").NodeLive;
+const Form = @import("../element/html/Form.zig");
const Mode = enum {
tag,
@@ -35,11 +36,13 @@ const Mode = enum {
selected_options,
links,
anchors,
+ form,
};
const HTMLCollection = @This();
-data: union(Mode) {
+_type: Type = .{ .generic = {} },
+_data: union(Mode) {
tag: NodeLive(.tag),
tag_name: NodeLive(.tag_name),
class_name: NodeLive(.class_name),
@@ -50,22 +53,28 @@ data: union(Mode) {
selected_options: NodeLive(.selected_options),
links: NodeLive(.links),
anchors: NodeLive(.anchors),
+ form: NodeLive(.form),
},
+const Type = union(enum) {
+ generic: void,
+ form: *Form,
+};
+
pub fn length(self: *HTMLCollection, page: *const Page) u32 {
- return switch (self.data) {
+ return switch (self._data) {
inline else => |*impl| impl.length(page),
};
}
pub fn getAtIndex(self: *HTMLCollection, index: usize, page: *const Page) ?*Element {
- return switch (self.data) {
+ return switch (self._data) {
inline else => |*impl| impl.getAtIndex(index, page),
};
}
pub fn getByName(self: *HTMLCollection, name: []const u8, page: *Page) ?*Element {
- return switch (self.data) {
+ return switch (self._data) {
inline else => |*impl| impl.getByName(name, page),
};
}
@@ -73,7 +82,7 @@ pub fn getByName(self: *HTMLCollection, name: []const u8, page: *Page) ?*Element
pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator {
return Iterator.init(.{
.list = self,
- .tw = switch (self.data) {
+ .tw = switch (self._data) {
.tag => |*impl| .{ .tag = impl._tw.clone() },
.tag_name => |*impl| .{ .tag_name = impl._tw.clone() },
.class_name => |*impl| .{ .class_name = impl._tw.clone() },
@@ -84,6 +93,7 @@ pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator {
.selected_options => |*impl| .{ .selected_options = impl._tw.clone() },
.links => |*impl| .{ .links = impl._tw.clone() },
.anchors => |*impl| .{ .anchors = impl._tw.clone() },
+ .form => |*impl| .{ .form = impl._tw.clone() },
},
}, page);
}
@@ -102,10 +112,11 @@ pub const Iterator = GenericIterator(struct {
selected_options: TreeWalker.Children,
links: TreeWalker.FullExcludeSelf,
anchors: TreeWalker.FullExcludeSelf,
+ form: TreeWalker.FullExcludeSelf,
},
pub fn next(self: *@This(), _: *Page) ?*Element {
- return switch (self.list.data) {
+ return switch (self.list._data) {
.tag => |*impl| impl.nextTw(&self.tw.tag),
.tag_name => |*impl| impl.nextTw(&self.tw.tag_name),
.class_name => |*impl| impl.nextTw(&self.tw.class_name),
@@ -116,6 +127,7 @@ pub const Iterator = GenericIterator(struct {
.selected_options => |*impl| impl.nextTw(&self.tw.selected_options),
.links => |*impl| impl.nextTw(&self.tw.links),
.anchors => |*impl| impl.nextTw(&self.tw.anchors),
+ .form => |*impl| impl.nextTw(&self.tw.form),
};
}
}, null);
diff --git a/src/browser/webapi/collections/HTMLFormControlsCollection.zig b/src/browser/webapi/collections/HTMLFormControlsCollection.zig
new file mode 100644
index 00000000..e7fd1420
--- /dev/null
+++ b/src/browser/webapi/collections/HTMLFormControlsCollection.zig
@@ -0,0 +1,57 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+const js = @import("../../js/js.zig");
+const Page = @import("../../Page.zig");
+const Element = @import("../Element.zig");
+const HTMLCollection = @import("HTMLCollection.zig");
+
+const HTMLFormControlsCollection = @This();
+
+_proto: *HTMLCollection,
+
+pub fn length(self: *HTMLFormControlsCollection, page: *Page) u32 {
+ return self._proto.length(page);
+}
+
+pub fn getAtIndex(self: *HTMLFormControlsCollection, index: usize, page: *Page) ?*Element {
+ return self._proto.getAtIndex(index, page);
+}
+
+pub fn namedItem(self: *HTMLFormControlsCollection, name: []const u8, page: *Page) ?*Element {
+ // TODO: When multiple elements have same name (radio buttons),
+ // should return RadioNodeList instead of first element
+ return self._proto.getByName(name, page);
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(HTMLFormControlsCollection);
+
+ pub const Meta = struct {
+ pub const name = "HTMLFormControlsCollection";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ pub const manage = false;
+ };
+
+ pub const length = bridge.accessor(HTMLFormControlsCollection.length, null, .{});
+ pub const @"[int]" = bridge.indexed(HTMLFormControlsCollection.getAtIndex, .{ .null_as_undefined = true });
+ pub const @"[str]" = bridge.namedIndexed(HTMLFormControlsCollection.namedItem, null, null, .{ .null_as_undefined = true });
+ pub const namedItem = bridge.function(HTMLFormControlsCollection.namedItem, .{});
+};
diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig
index f3f4dd1a..f02b66fb 100644
--- a/src/browser/webapi/collections/node_live.zig
+++ b/src/browser/webapi/collections/node_live.zig
@@ -28,6 +28,7 @@ const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig");
const Selector = @import("../selector/Selector.zig");
+const Form = @import("../element/html/Form.zig");
const Allocator = std.mem.Allocator;
@@ -42,6 +43,7 @@ const Mode = enum {
selected_options,
links,
anchors,
+ form,
};
const Filters = union(Mode) {
@@ -55,6 +57,7 @@ const Filters = union(Mode) {
selected_options,
links,
anchors,
+ form: *Form,
fn TypeOf(comptime mode: Mode) type {
@setEvalBranchQuota(2000);
@@ -82,7 +85,7 @@ const Filters = union(Mode) {
pub fn NodeLive(comptime mode: Mode) type {
const Filter = Filters.TypeOf(mode);
const TW = switch (mode) {
- .tag, .tag_name, .class_name, .name, .all_elements, .links, .anchors => TreeWalker.FullExcludeSelf,
+ .tag, .tag_name, .class_name, .name, .all_elements, .links, .anchors, .form => TreeWalker.FullExcludeSelf,
.child_elements, .child_tag, .selected_options => TreeWalker.Children,
};
return struct {
@@ -259,9 +262,46 @@ pub fn NodeLive(comptime mode: Mode) type {
if (el.is(Anchor) == null) return false;
return el.hasAttributeSafe("name");
},
+ .form => {
+ const el = node.is(Element) orelse return false;
+ if (!isFormControl(el)) {
+ return false;
+ }
+
+ if (el.getAttributeSafe("form")) |form_attr| {
+ const form_id = self._filter.asElement().getAttributeSafe("id") orelse return false;
+ return std.mem.eql(u8, form_attr, form_id);
+ }
+
+ // No form attribute - match if descendant of our form
+ // This does an O(depth) ancestor walk for each control in the form.
+ //
+ // TODO: If profiling shows this is a bottleneck:
+ // When we first encounter the form element during tree walk, we could
+ // do a one-time reverse walk to find the LAST control that belongs to
+ // this form (checking both form controls and their form= attributes).
+ // Store that element in a new FormState. Then as we traverse
+ // forward:
+ // - Set is_within_form = true when we enter the form element
+ // - Return true immediately for any control while is_within_form
+ // - Set is_within_form = false when we reach that last element
+ // This trades one O(form_size) reverse walk for N O(depth) ancestor
+ // checks, where N = number of controls. For forms with many nested
+ // controls, this could be significantly faster.
+ return self._filter.asNode().contains(node);
+ },
}
}
+ fn isFormControl(el: *Element) bool {
+ if (el._type != .html) return false;
+ const html = el._type.html;
+ return switch (html._type) {
+ .input, .button, .select, .text_area => true,
+ else => false,
+ };
+ }
+
fn versionCheck(self: *Self, page: *const Page) bool {
const current = page.version;
if (current == self._cached_version) {
@@ -278,16 +318,17 @@ pub fn NodeLive(comptime mode: Mode) type {
const HTMLCollection = @import("HTMLCollection.zig");
pub fn runtimeGenericWrap(self: Self, page: *Page) !*HTMLCollection {
const collection = switch (mode) {
- .tag => HTMLCollection{ .data = .{ .tag = self } },
- .tag_name => HTMLCollection{ .data = .{ .tag_name = self } },
- .class_name => HTMLCollection{ .data = .{ .class_name = self } },
- .name => HTMLCollection{ .data = .{ .name = self } },
- .all_elements => HTMLCollection{ .data = .{ .all_elements = self } },
- .child_elements => HTMLCollection{ .data = .{ .child_elements = self } },
- .child_tag => HTMLCollection{ .data = .{ .child_tag = self } },
- .selected_options => HTMLCollection{ .data = .{ .selected_options = self } },
- .links => HTMLCollection{ .data = .{ .links = self } },
- .anchors => HTMLCollection{ .data = .{ .anchors = self } },
+ .tag => HTMLCollection{ ._data = .{ .tag = self } },
+ .tag_name => HTMLCollection{ ._data = .{ .tag_name = self } },
+ .class_name => HTMLCollection{ ._data = .{ .class_name = self } },
+ .name => HTMLCollection{ ._data = .{ .name = self } },
+ .all_elements => HTMLCollection{ ._data = .{ .all_elements = self } },
+ .child_elements => HTMLCollection{ ._data = .{ .child_elements = self } },
+ .child_tag => HTMLCollection{ ._data = .{ .child_tag = self } },
+ .selected_options => HTMLCollection{ ._data = .{ .selected_options = self } },
+ .links => HTMLCollection{ ._data = .{ .links = self } },
+ .anchors => HTMLCollection{ ._data = .{ .anchors = self } },
+ .form => HTMLCollection{ ._type = .{ .form = self._filter }, ._data = .{ .form = self } },
};
return page._factory.create(collection);
}
diff --git a/src/browser/webapi/element/html/Form.zig b/src/browser/webapi/element/html/Form.zig
index 66f23dde..4e3186a2 100644
--- a/src/browser/webapi/element/html/Form.zig
+++ b/src/browser/webapi/element/html/Form.zig
@@ -23,6 +23,7 @@ const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const TreeWalker = @import("../../TreeWalker.zig");
+const collections = @import("../../collections.zig");
const Input = @import("Input.zig");
const Button = @import("Button.zig");
@@ -32,6 +33,9 @@ const TextArea = @import("TextArea.zig");
const Form = @This();
_proto: *HtmlElement,
+fn asConstElement(self: *const Form) *const Element {
+ return self._proto._proto;
+}
pub fn asElement(self: *Form) *Element {
return self._proto._proto;
}
@@ -39,90 +43,49 @@ pub fn asNode(self: *Form) *Node {
return self.asElement().asNode();
}
-// Untested / unused right now. Iterates over all the controls of a form,
-// including those outside the but with a form=$FORM_ID attribute
-pub const Iterator = struct {
- _form_id: ?[]const u8,
- _walkers: union(enum) {
- nested: TreeWalker.FullExcludeSelf,
- names: TreeWalker.FullExcludeSelf,
- },
+pub fn getName(self: *const Form) []const u8 {
+ return self.asConstElement().getAttributeSafe("name") orelse "";
+}
- pub fn init(form: *Form) Iterator {
- const form_element = form.asElement();
- const form_id = form_element.getAttributeSafe("id");
+pub fn setName(self: *Form, name: []const u8, page: *Page) !void {
+ try self.asElement().setAttributeSafe("name", name, page);
+}
- return .{
- ._form_id = form_id,
- ._walkers = .{
- .nested = TreeWalker.FullExcludeSelf.init(form.asNode(), .{}),
- },
- };
+pub fn getMethod(self: *const Form) []const u8 {
+ const method = self.asConstElement().getAttributeSafe("method") orelse return "get";
+
+ if (std.ascii.eqlIgnoreCase(method, "post")) {
+ return "post";
}
-
- pub fn next(self: *Iterator) ?FormControl {
- switch (self._walkers) {
- .nested => |*tw| {
- // find controls nested directly in the form
- while (tw.next()) |node| {
- const element = node.is(Element) orelse continue;
- const control = asFormControl(element) orelse continue;
- // Skip if it has a form attribute (will be handled in phase 2)
- if (element.getAttributeSafe("form") == null) {
- return control;
- }
- }
- if (self._form_id == null) {
- return null;
- }
-
- const doc = tw._root.getRootNode();
- self._walkers = .{
- .names = TreeWalker.FullExcludeSelf(doc, .{}),
- };
- return self.next();
- },
- .names => |*tw| {
- // find controls with a name matching the form id
- while (tw.next()) |node| {
- const input = node.is(Input) orelse continue;
- if (input._type != .radio) {
- continue;
- }
- const input_form = input.asElement().getAttributeSafe("form") orelse continue;
- // must have a self._form_id, else we never would have transitioned
- // from a nested walker to a namew walker
- if (!std.mem.eql(u8, input_form, self._form_id.?)) {
- continue;
- }
- return .{ .input = input };
- }
-
- return null;
- },
- }
+ if (std.ascii.eqlIgnoreCase(method, "dialog")) {
+ return "dialog";
}
-};
+ // invalid, or it was get all along
+ return "get";
+}
-pub const FormControl = union(enum) {
- input: *Input,
- button: *Button,
- select: *Select,
- textarea: *TextArea,
-};
+pub fn setMethod(self: *Form, method: []const u8, page: *Page) !void {
+ try self.asElement().setAttributeSafe("method", method, page);
+}
-fn asFormControl(element: *Element) ?FormControl {
- if (element._type != .html) {
- return null;
- }
- const html = element._type.html;
- switch (html._type) {
- .input => |cntrl| return .{ .input = cntrl },
- .button => |cntrl| return .{ .button = cntrl },
- .select => |cntrl| return .{ .select = cntrl },
- .textarea => |cntrl| return .{ .textarea = cntrl },
- else => return null,
- }
+pub fn getElements(self: *Form, page: *Page) !*collections.HTMLFormControlsCollection {
+ const form_id = self.asElement().getAttributeSafe("id");
+ const root = if (form_id != null)
+ self.asNode().getRootNode(null) // Has ID: walk entire document to find form=ID controls
+ else
+ self.asNode(); // No ID: walk only form subtree (no external controls possible)
+
+ const node_live = collections.NodeLive(.form).init(root, self, page);
+ const html_collection = try node_live.runtimeGenericWrap(page);
+
+ return page._factory.create(collections.HTMLFormControlsCollection{
+ ._proto = html_collection,
+ });
+}
+
+pub fn getLength(self: *Form, page: *Page) !u32 {
+ const elements = try self.getElements(page);
+ return elements.length(page);
}
pub const JsApi = struct {
@@ -132,4 +95,14 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
+
+ pub const name = bridge.accessor(Form.getName, Form.setName, .{});
+ pub const method = bridge.accessor(Form.getMethod, Form.setMethod, .{});
+ pub const elements = bridge.accessor(Form.getElements, null, .{});
+ pub const length = bridge.accessor(Form.getLength, null, .{});
};
+
+const testing = @import("../../../../testing.zig");
+test "WebApi: HTML.Form" {
+ try testing.htmlRunner("element/html/form.html", .{});
+}
diff --git a/src/browser/webapi/net/FormData.zig b/src/browser/webapi/net/FormData.zig
index 610c88bf..f8655246 100644
--- a/src/browser/webapi/net/FormData.zig
+++ b/src/browser/webapi/net/FormData.zig
@@ -22,6 +22,8 @@ const log = @import("../../../log.zig");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
+const Form = @import("../element/html/Form.zig");
+const Element = @import("../Element.zig");
const KeyValueList = @import("../KeyValueList.zig");
const Alloctor = std.mem.Allocator;
@@ -31,7 +33,9 @@ const FormData = @This();
_arena: Alloctor,
_list: KeyValueList,
-pub fn init(page: *Page) !*FormData {
+pub fn init(form_: ?*Form, submitter_: ?*Element, page: *Page) !*FormData {
+ _ = form_;
+ _ = submitter_;
return page._factory.create(FormData{
._arena = page.arena,
._list = KeyValueList.init(),
@@ -127,6 +131,97 @@ pub const JsApi = struct {
pub const forEach = bridge.function(FormData.forEach, .{});
};
+// fn collectForm(form: *Form, submitter_: ?*Element, page: *Page) !KeyValueList {
+// const arena = page.arena;
+
+// // Don't use libdom's formGetCollection (aka dom_html_form_element_get_elements)
+// // It doesn't work with dynamically added elements, because their form
+// // property doesn't get set. We should fix that.
+// // However, even once fixed, there are other form-collection features we
+// // probably want to implement (like disabled fieldsets), so we might want
+// // to stick with our own walker even if fix libdom to properly support
+// // dynamically added elements.
+// const node_list = try @import("../dom/css.zig").querySelectorAll(arena, @ptrCast(@alignCast(form)), "input,select,button,textarea");
+// const nodes = node_list.nodes.items;
+
+// var entries: kv.List = .{};
+// try entries.ensureTotalCapacity(arena, nodes.len);
+
+// var submitter_included = false;
+// const submitter_name_ = try getSubmitterName(submitter_);
+
+// for (nodes) |node| {
+// const element = parser.nodeToElement(node);
+
+// // must have a name
+// const name = try parser.elementGetAttribute(element, "name") orelse continue;
+// if (try parser.elementGetAttribute(element, "disabled") != null) {
+// continue;
+// }
+
+// const tag = try parser.elementTag(element);
+// switch (tag) {
+// .input => {
+// const tpe = try parser.inputGetType(@ptrCast(element));
+// if (std.ascii.eqlIgnoreCase(tpe, "image")) {
+// if (submitter_name_) |submitter_name| {
+// if (std.mem.eql(u8, submitter_name, name)) {
+// const key_x = try std.fmt.allocPrint(arena, "{s}.x", .{name});
+// const key_y = try std.fmt.allocPrint(arena, "{s}.y", .{name});
+// try entries.appendOwned(arena, key_x, "0");
+// try entries.appendOwned(arena, key_y, "0");
+// submitter_included = true;
+// }
+// }
+// continue;
+// }
+
+// if (std.ascii.eqlIgnoreCase(tpe, "checkbox") or std.ascii.eqlIgnoreCase(tpe, "radio")) {
+// if (try parser.inputGetChecked(@ptrCast(element)) == false) {
+// continue;
+// }
+// }
+// if (std.ascii.eqlIgnoreCase(tpe, "submit")) {
+// if (submitter_name_ == null or !std.mem.eql(u8, submitter_name_.?, name)) {
+// continue;
+// }
+// submitter_included = true;
+// }
+// const value = try parser.inputGetValue(@ptrCast(element));
+// try entries.appendOwned(arena, name, value);
+// },
+// .select => {
+// const select: *parser.Select = @ptrCast(node);
+// try collectSelectValues(arena, select, name, &entries, page);
+// },
+// .textarea => {
+// const textarea: *parser.TextArea = @ptrCast(node);
+// const value = try parser.textareaGetValue(textarea);
+// try entries.appendOwned(arena, name, value);
+// },
+// .button => if (submitter_name_) |submitter_name| {
+// if (std.mem.eql(u8, submitter_name, name)) {
+// const value = (try parser.elementGetAttribute(element, "value")) orelse "";
+// try entries.appendOwned(arena, name, value);
+// submitter_included = true;
+// }
+// },
+// else => unreachable,
+// }
+// }
+
+// if (submitter_included == false) {
+// if (submitter_name_) |submitter_name| {
+// // 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.appendOwned(arena, submitter_name, value);
+// }
+// }
+
+// return entries;
+// }
+
const testing = @import("../../../testing.zig");
test "WebApi: FormData" {
try testing.htmlRunner("net/form_data.html", .{});