Merge branch 'main' into agent

This commit is contained in:
Adrià Arrufat
2026-05-14 09:24:12 +02:00
8 changed files with 472 additions and 23 deletions

View File

@@ -3994,13 +3994,14 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For
}
break :blk form_element.getAttributeSafe(comptime .wrap("enctype"));
};
const method = blk: {
const method_attr: ?[]const u8 = blk: {
if (submit_button) |s| {
if (s.getAttributeSafe(comptime .wrap("formmethod"))) |fm| break :blk fm;
}
break :blk form_element.getAttributeSafe(comptime .wrap("method")) orelse "";
break :blk form_element.getAttributeSafe(comptime .wrap("method"));
};
const is_post = std.ascii.eqlIgnoreCase(method, "post");
const method = Element.Html.Form.normalizeMethod(method_attr, "get");
const is_post = std.mem.eql(u8, method, "post");
// Get charset from accept-charset attribute or fall back to document charset
const charset: []const u8 = blk: {
@@ -4017,16 +4018,14 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For
var boundary_buf: [36]u8 = undefined;
// GET ignores enctype per HTML spec; only resolve the union for POST.
const encoding: FormData.EncType = blk: {
if (is_post) {
if (enctype_attr) |attr| {
if (std.ascii.eqlIgnoreCase(attr, "multipart/form-data")) {
@import("../id.zig").uuidv4(&boundary_buf);
break :blk .{ .formdata = &boundary_buf };
}
if (!std.ascii.eqlIgnoreCase(attr, "application/x-www-form-urlencoded")) {
log.warn(.not_implemented, "FormData.encoding", .{ .encoding = attr });
}
}
if (!is_post) break :blk .urlencode;
const canonical = Element.Html.Form.normalizeEnctype(enctype_attr, "application/x-www-form-urlencoded");
if (std.mem.eql(u8, canonical, "multipart/form-data")) {
@import("../id.zig").uuidv4(&boundary_buf);
break :blk .{ .formdata = &boundary_buf };
}
if (std.mem.eql(u8, canonical, "text/plain")) {
break :blk .plaintext;
}
break :blk .urlencode;
};
@@ -4051,6 +4050,9 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For
opts.header = switch (encoding) {
.urlencode => "Content-Type: application/x-www-form-urlencoded",
.formdata => |b| try std.fmt.allocPrintSentinel(arena, "Content-Type: multipart/form-data; boundary={s}", .{b}, 0),
// Per WHATWG HTML §4.10.21.6, text/plain submissions include the form's
// resolved encoding (accept-charset or document charset).
.plaintext => try std.fmt.allocPrintSentinel(arena, "Content-Type: text/plain; charset={s}", .{charset}, 0),
};
} else {
action = try URL.concatQueryString(arena, action, buf.written());

View File

@@ -142,3 +142,120 @@
testing.expectEqual(true, captured.isTrusted)
}
</script>
<!-- Test fixtures for form submission attribute overrides -->
<button id="btn_no_overrides"></button>
<button id="btn_with_overrides"
formaction="/override" formmethod="POST"
formenctype="text/plain" formtarget="_blank" formnovalidate></button>
<button id="btn_invalid_overrides"
formmethod="WEIRD" formenctype="bogus"></button>
<script id="form_action_initial">
// Missing: returns the document URL per spec.
testing.expectEqual(testing.BASE_URL + 'element/html/button.html', $('#btn_no_overrides').formAction)
// Present: resolved against the document URL.
testing.expectEqual(testing.ORIGIN + '/override', $('#btn_with_overrides').formAction)
</script>
<script id="form_action_set">
{
const b = document.createElement('button')
testing.expectEqual(testing.BASE_URL + 'element/html/button.html', b.formAction)
b.formAction = 'hello'
testing.expectEqual(testing.BASE_URL + 'element/html/hello', b.formAction)
b.formAction = '/hello'
testing.expectEqual(testing.ORIGIN + '/hello', b.formAction)
b.formAction = 'https://lightpanda.io/hello'
testing.expectEqual('https://lightpanda.io/hello', b.formAction)
}
</script>
<script id="form_enctype_initial">
// Missing-value default on overrides: empty string (so `submitter.formEnctype ||
// form.enctype` falls through to the form's value).
testing.expectEqual('', $('#btn_no_overrides').formEnctype)
testing.expectEqual('text/plain', $('#btn_with_overrides').formEnctype)
// Invalid-value default: "application/x-www-form-urlencoded"
testing.expectEqual('application/x-www-form-urlencoded', $('#btn_invalid_overrides').formEnctype)
</script>
<script id="form_enctype_set">
{
const b = document.createElement('button')
testing.expectEqual('', b.formEnctype)
b.formEnctype = 'multipart/form-data'
testing.expectEqual('multipart/form-data', b.formEnctype)
testing.expectEqual('multipart/form-data', b.getAttribute('formenctype'))
}
</script>
<script id="form_method_initial">
testing.expectEqual('', $('#btn_no_overrides').formMethod)
testing.expectEqual('post', $('#btn_with_overrides').formMethod)
// Invalid-value default: "get"
testing.expectEqual('get', $('#btn_invalid_overrides').formMethod)
</script>
<script id="form_method_normalization">
{
const b = document.createElement('button')
testing.expectEqual('', b.formMethod)
b.setAttribute('formmethod', 'POST')
testing.expectEqual('post', b.formMethod)
b.setAttribute('formmethod', 'DIALOG')
testing.expectEqual('dialog', b.formMethod)
b.setAttribute('formmethod', '')
testing.expectEqual('get', b.formMethod)
b.formMethod = 'GET'
testing.expectEqual('get', b.formMethod)
testing.expectEqual('GET', b.getAttribute('formmethod'))
}
</script>
<script id="form_target_initial">
testing.expectEqual('', $('#btn_no_overrides').formTarget)
testing.expectEqual('_blank', $('#btn_with_overrides').formTarget)
</script>
<script id="form_target_set">
{
const b = document.createElement('button')
testing.expectEqual('', b.formTarget)
b.formTarget = '_self'
testing.expectEqual('_self', b.formTarget)
testing.expectEqual('_self', b.getAttribute('formtarget'))
}
</script>
<script id="form_no_validate_initial">
testing.expectEqual(false, $('#btn_no_overrides').formNoValidate)
testing.expectEqual(true, $('#btn_with_overrides').formNoValidate)
</script>
<script id="form_no_validate_set">
{
const b = document.createElement('button')
testing.expectEqual(false, b.formNoValidate)
b.formNoValidate = true
testing.expectEqual(true, b.formNoValidate)
testing.expectEqual('', b.getAttribute('formnovalidate'))
b.formNoValidate = false
testing.expectEqual(false, b.formNoValidate)
testing.expectEqual(null, b.getAttribute('formnovalidate'))
}
</script>

View File

@@ -85,6 +85,51 @@
}
</script>
<!-- Test fixtures for form.enctype -->
<form id="form_enctype_default"></form>
<form id="form_enctype_url" enctype="application/x-www-form-urlencoded"></form>
<form id="form_enctype_multipart" enctype="multipart/form-data"></form>
<form id="form_enctype_plain" enctype="text/plain"></form>
<script id="enctype_initial">
// Missing-value default: "application/x-www-form-urlencoded"
testing.expectEqual('application/x-www-form-urlencoded', $('#form_enctype_default').enctype)
testing.expectEqual('application/x-www-form-urlencoded', $('#form_enctype_url').enctype)
testing.expectEqual('multipart/form-data', $('#form_enctype_multipart').enctype)
testing.expectEqual('text/plain', $('#form_enctype_plain').enctype)
</script>
<script id="enctype_set">
{
const form = document.createElement('form')
testing.expectEqual('application/x-www-form-urlencoded', form.enctype)
form.enctype = 'multipart/form-data'
testing.expectEqual('multipart/form-data', form.enctype)
testing.expectEqual('multipart/form-data', form.getAttribute('enctype'))
}
</script>
<script id="enctype_normalization">
{
const form = document.createElement('form')
// Case-insensitive match against known values
form.setAttribute('enctype', 'MULTIPART/FORM-DATA')
testing.expectEqual('multipart/form-data', form.enctype)
form.setAttribute('enctype', 'Text/Plain')
testing.expectEqual('text/plain', form.enctype)
// Invalid-value default: "application/x-www-form-urlencoded"
form.setAttribute('enctype', 'application/json')
testing.expectEqual('application/x-www-form-urlencoded', form.enctype)
form.setAttribute('enctype', '')
testing.expectEqual('application/x-www-form-urlencoded', form.enctype)
}
</script>
<!-- Test fixtures for form.elements -->
<form id="form1">
<input name="field1" value="value1">

View File

@@ -287,3 +287,80 @@
testing.expectEqual('5', s12.value);
}
</script>
<!-- Test fixtures for form submission attribute overrides -->
<input id="inp_no_overrides" type="submit">
<input id="inp_with_overrides" type="submit"
formaction="/override" formmethod="POST"
formenctype="text/plain" formtarget="_blank" formnovalidate>
<input id="inp_invalid_overrides" type="submit"
formmethod="WEIRD" formenctype="bogus">
<script id="input_form_action_initial">
// Missing: returns the document URL per spec.
testing.expectEqual(testing.BASE_URL + 'element/html/input-attrs.html', $('#inp_no_overrides').formAction)
testing.expectEqual(testing.ORIGIN + '/override', $('#inp_with_overrides').formAction)
</script>
<script id="input_form_action_set">
{
const i = document.createElement('input')
i.type = 'submit'
testing.expectEqual(testing.BASE_URL + 'element/html/input-attrs.html', i.formAction)
i.formAction = '/hello'
testing.expectEqual(testing.ORIGIN + '/hello', i.formAction)
}
</script>
<script id="input_form_enctype_initial">
testing.expectEqual('', $('#inp_no_overrides').formEnctype)
testing.expectEqual('text/plain', $('#inp_with_overrides').formEnctype)
testing.expectEqual('application/x-www-form-urlencoded', $('#inp_invalid_overrides').formEnctype)
</script>
<script id="input_form_method_initial">
testing.expectEqual('', $('#inp_no_overrides').formMethod)
testing.expectEqual('post', $('#inp_with_overrides').formMethod)
testing.expectEqual('get', $('#inp_invalid_overrides').formMethod)
</script>
<script id="input_form_method_normalization">
{
const i = document.createElement('input')
i.type = 'submit'
testing.expectEqual('', i.formMethod)
i.setAttribute('formmethod', 'POST')
testing.expectEqual('post', i.formMethod)
i.setAttribute('formmethod', '')
testing.expectEqual('get', i.formMethod)
}
</script>
<script id="input_form_target_initial">
testing.expectEqual('', $('#inp_no_overrides').formTarget)
testing.expectEqual('_blank', $('#inp_with_overrides').formTarget)
</script>
<script id="input_form_no_validate_initial">
testing.expectEqual(false, $('#inp_no_overrides').formNoValidate)
testing.expectEqual(true, $('#inp_with_overrides').formNoValidate)
</script>
<script id="input_form_no_validate_set">
{
const i = document.createElement('input')
i.type = 'submit'
testing.expectEqual(false, i.formNoValidate)
i.formNoValidate = true
testing.expectEqual(true, i.formNoValidate)
testing.expectEqual('', i.getAttribute('formnovalidate'))
i.formNoValidate = false
testing.expectEqual(false, i.formNoValidate)
testing.expectEqual(null, i.getAttribute('formnovalidate'))
}
</script>

View File

@@ -120,6 +120,65 @@ pub fn getLabels(self: *Button, frame: *Frame) !js.Array {
return @import("Label.zig").getControlLabels(self.asElement(), frame);
}
// Form submission attribute overrides
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-0
//
// Each `formX` IDL attribute reflects the matching `formx` content attribute and
// overrides the form-owner's value when this button is the submitter. Per spec
// these are "no missing value default" reflections — getters return "" when the
// content attribute is absent, so the `submitter.formX || form.X` idiom in
// downstream CDP clients (e.g. Turbo's FormSubmission constructor) falls
// through to the form's value.
pub fn getFormAction(self: *Button, frame: *Frame) ![]const u8 {
const element = self.asElement();
const action = element.getAttributeSafe(comptime .wrap("formaction")) orelse return frame.url;
if (action.len == 0) {
return frame.url;
}
return element.asNode().resolveURL(action, frame, .{});
}
pub fn setFormAction(self: *Button, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("formaction"), .wrap(value), frame);
}
pub fn getFormEnctype(self: *const Button) []const u8 {
return Form.normalizeEnctype(self.asConstElement().getAttributeSafe(comptime .wrap("formenctype")), "");
}
pub fn setFormEnctype(self: *Button, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("formenctype"), .wrap(value), frame);
}
pub fn getFormMethod(self: *const Button) []const u8 {
return Form.normalizeMethod(self.asConstElement().getAttributeSafe(comptime .wrap("formmethod")), "");
}
pub fn setFormMethod(self: *Button, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("formmethod"), .wrap(value), frame);
}
pub fn getFormTarget(self: *const Button) []const u8 {
return self.asConstElement().getAttributeSafe(comptime .wrap("formtarget")) orelse "";
}
pub fn setFormTarget(self: *Button, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("formtarget"), .wrap(value), frame);
}
pub fn getFormNoValidate(self: *const Button) bool {
return self.asConstElement().getAttributeSafe(.wrap("formnovalidate")) != null;
}
pub fn setFormNoValidate(self: *Button, value: bool, frame: *Frame) !void {
if (value) {
try self.asElement().setAttributeSafe(.wrap("formnovalidate"), .wrap(""), frame);
} else {
try self.asElement().removeAttribute(.wrap("formnovalidate"), frame);
}
}
// Constraint validation
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#the-constraint-validation-api
//
@@ -182,6 +241,11 @@ pub const JsApi = struct {
pub const name = bridge.accessor(Button.getName, Button.setName, .{});
pub const required = bridge.accessor(Button.getRequired, Button.setRequired, .{});
pub const form = bridge.accessor(Button.getForm, null, .{});
pub const formAction = bridge.accessor(Button.getFormAction, Button.setFormAction, .{});
pub const formEnctype = bridge.accessor(Button.getFormEnctype, Button.setFormEnctype, .{});
pub const formMethod = bridge.accessor(Button.getFormMethod, Button.setFormMethod, .{});
pub const formNoValidate = bridge.accessor(Button.getFormNoValidate, Button.setFormNoValidate, .{});
pub const formTarget = bridge.accessor(Button.getFormTarget, Button.setFormTarget, .{});
pub const value = bridge.accessor(Button.getValue, Button.setValue, .{});
pub const @"type" = bridge.accessor(Button.getType, Button.setType, .{});
pub const labels = bridge.accessor(Button.getLabels, null, .{});

View File

@@ -54,19 +54,34 @@ pub fn setName(self: *Form, name: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(name), frame);
}
pub fn getMethod(self: *const Form) []const u8 {
const method = self.asConstElement().getAttributeSafe(comptime .wrap("method")) orelse return "get";
if (std.ascii.eqlIgnoreCase(method, "post")) {
return "post";
}
if (std.ascii.eqlIgnoreCase(method, "dialog")) {
return "dialog";
}
// invalid, or it was get all along
/// Canonicalize the `method` content attribute (or its `formmethod` submitter
/// override) per WHATWG HTML "limited to only known values":
/// - missing → returns `missing_default`
/// - "post" / "dialog" → returns the lowercased keyword
/// - empty / invalid / "get" → returns "get" (invalid-value default)
pub fn normalizeMethod(attr: ?[]const u8, missing_default: []const u8) []const u8 {
const method = attr orelse return missing_default;
if (std.ascii.eqlIgnoreCase(method, "post")) return "post";
if (std.ascii.eqlIgnoreCase(method, "dialog")) return "dialog";
return "get";
}
/// Canonicalize the `enctype` content attribute (or its `formenctype` submitter
/// override) per WHATWG HTML "limited to only known values":
/// - missing → returns `missing_default`
/// - "multipart/form-data" / "text/plain" → returns the lowercased keyword
/// - empty / invalid / urlencoded → returns "application/x-www-form-urlencoded"
pub fn normalizeEnctype(attr: ?[]const u8, missing_default: []const u8) []const u8 {
const enctype = attr orelse return missing_default;
if (std.ascii.eqlIgnoreCase(enctype, "multipart/form-data")) return "multipart/form-data";
if (std.ascii.eqlIgnoreCase(enctype, "text/plain")) return "text/plain";
return "application/x-www-form-urlencoded";
}
pub fn getMethod(self: *const Form) []const u8 {
return normalizeMethod(self.asConstElement().getAttributeSafe(comptime .wrap("method")), "get");
}
pub fn setMethod(self: *Form, method: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("method"), .wrap(method), frame);
}
@@ -119,6 +134,14 @@ pub fn setAcceptCharset(self: *Form, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(.wrap("accept-charset"), .wrap(value), frame);
}
pub fn getEnctype(self: *const Form) []const u8 {
return normalizeEnctype(self.asConstElement().getAttributeSafe(comptime .wrap("enctype")), "application/x-www-form-urlencoded");
}
pub fn setEnctype(self: *Form, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("enctype"), .wrap(value), frame);
}
pub fn getLength(self: *Form, frame: *Frame) !u32 {
const elements = try self.getElements(frame);
return elements.length(frame);
@@ -209,6 +232,7 @@ pub const JsApi = struct {
pub const action = bridge.accessor(Form.getAction, Form.setAction, .{});
pub const target = bridge.accessor(Form.getTarget, Form.setTarget, .{});
pub const acceptCharset = bridge.accessor(Form.getAcceptCharset, Form.setAcceptCharset, .{});
pub const enctype = bridge.accessor(Form.getEnctype, Form.setEnctype, .{});
pub const elements = bridge.accessor(Form.getElements, null, .{});
pub const length = bridge.accessor(Form.getLength, null, .{});
pub const submit = bridge.function(Form.submit, .{});

View File

@@ -834,6 +834,59 @@ pub fn getForm(self: *const Input, frame: *Frame) ?*Form {
return null;
}
// Form submission attribute overrides
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-0
// Mirrors Button's overrides — same spec semantics.
pub fn getFormAction(self: *Input, frame: *Frame) ![]const u8 {
const element = self.asElement();
const action = element.getAttributeSafe(comptime .wrap("formaction")) orelse return frame.url;
if (action.len == 0) {
return frame.url;
}
return element.asNode().resolveURL(action, frame, .{});
}
pub fn setFormAction(self: *Input, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("formaction"), .wrap(value), frame);
}
pub fn getFormEnctype(self: *const Input) []const u8 {
return Form.normalizeEnctype(self.asConstElement().getAttributeSafe(comptime .wrap("formenctype")), "");
}
pub fn setFormEnctype(self: *Input, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("formenctype"), .wrap(value), frame);
}
pub fn getFormMethod(self: *const Input) []const u8 {
return Form.normalizeMethod(self.asConstElement().getAttributeSafe(comptime .wrap("formmethod")), "");
}
pub fn setFormMethod(self: *Input, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("formmethod"), .wrap(value), frame);
}
pub fn getFormTarget(self: *const Input) []const u8 {
return self.asConstElement().getAttributeSafe(comptime .wrap("formtarget")) orelse "";
}
pub fn setFormTarget(self: *Input, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("formtarget"), .wrap(value), frame);
}
pub fn getFormNoValidate(self: *const Input) bool {
return self.asConstElement().getAttributeSafe(.wrap("formnovalidate")) != null;
}
pub fn setFormNoValidate(self: *Input, value: bool, frame: *Frame) !void {
if (value) {
try self.asElement().setAttributeSafe(.wrap("formnovalidate"), .wrap(""), frame);
} else {
try self.asElement().removeAttribute(.wrap("formnovalidate"), frame);
}
}
/// Sanitize the value according to the current input type
fn sanitizeValue(self: *Input, comptime dupe: bool, value: []const u8, frame: *Frame) ![]const u8 {
switch (self._input_type) {
@@ -1265,6 +1318,11 @@ pub const JsApi = struct {
pub const size = bridge.accessor(Input.getSize, Input.setSize, .{});
pub const src = bridge.accessor(Input.getSrc, Input.setSrc, .{});
pub const form = bridge.accessor(Input.getForm, null, .{});
pub const formAction = bridge.accessor(Input.getFormAction, Input.setFormAction, .{});
pub const formEnctype = bridge.accessor(Input.getFormEnctype, Input.setFormEnctype, .{});
pub const formMethod = bridge.accessor(Input.getFormMethod, Input.setFormMethod, .{});
pub const formNoValidate = bridge.accessor(Input.getFormNoValidate, Input.setFormNoValidate, .{});
pub const formTarget = bridge.accessor(Input.getFormTarget, Input.setFormTarget, .{});
pub const labels = bridge.accessor(Input.getLabels, null, .{});
pub const indeterminate = bridge.accessor(Input.getIndeterminate, Input.setIndeterminate, .{});
pub const placeholder = bridge.accessor(Input.getPlaceholder, Input.setPlaceholder, .{});

View File

@@ -50,6 +50,13 @@ pub const Entry = struct {
.file => unreachable, // nothing currently creates this type of value
};
}
pub fn format(self: Value, writer: *std.Io.Writer) !void {
return switch (self) {
.string => |s| s.format(writer),
.file => unreachable, // nothing currently creates this type of value
};
}
};
};
@@ -163,6 +170,7 @@ pub const EncType = union(enum) {
urlencode,
// Boundary delimiter; caller owns the bytes (must outlive the write).
formdata: []const u8,
plaintext,
};
pub const WriteOpts = struct {
@@ -175,6 +183,7 @@ pub fn write(self: *const FormData, opts: WriteOpts, writer: *std.Io.Writer) !vo
switch (opts.encoding) {
.urlencode => return self.urlEncode(opts, writer),
.formdata => |boundary| return self.multipartEncode(boundary, writer),
.plaintext => return self.plaintextEncode(writer),
}
}
@@ -195,6 +204,20 @@ fn urlEncodeEntry(entry: *const Entry, opts: WriteOpts, writer: *std.Io.Writer)
try KeyValueList.urlEncodeFormValue(entry.value.asString(), opts.allocator, opts.charset, writer);
}
/// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#text/plain-encoding-algorithm
///
/// For each entry: name, "=", value, CRLF. No URL-encoding, no escaping. Per the
/// spec this is a low-fidelity encoding intended for human-readable values; a
/// value containing "=" or CRLF produces an ambiguous wire format, by design.
fn plaintextEncode(self: *const FormData, writer: *std.Io.Writer) !void {
for (self._entries.items) |*entry| {
try entry.name.format(writer);
try writer.writeByte('=');
try entry.value.format(writer);
try writer.writeAll("\r\n");
}
}
fn multipartEncode(self: *const FormData, boundary: []const u8, writer: *std.Io.Writer) !void {
for (self._entries.items) |*entry| {
try multipartEncodeEntry(entry, boundary, writer);
@@ -461,3 +484,42 @@ test "FormData: multipart empty body" {
try testing.expectString("--B--\r\n", buf.written());
}
test "FormData: plaintext write" {
const allocator = testing.arena_allocator;
var fd = FormData{
._arena = allocator,
._entries = .empty,
};
try fd.append("name", "John");
try fd.append("note", "two\r\nlines");
try fd.append("equals", "a=b");
var buf = std.Io.Writer.Allocating.init(allocator);
try fd.write(.{ .encoding = .plaintext, .allocator = allocator }, &buf.writer);
// Per WHATWG HTML text/plain encoding algorithm: name=value CRLF per entry.
// Values containing "=" or CRLF are written verbatim — the spec accepts that
// text/plain is a low-fidelity, lossy encoding for human-readable content.
try testing.expectString(
"name=John\r\n" ++
"note=two\r\nlines\r\n" ++
"equals=a=b\r\n",
buf.written(),
);
}
test "FormData: plaintext empty body" {
const allocator = testing.arena_allocator;
var fd = FormData{
._arena = allocator,
._entries = .empty,
};
var buf = std.Io.Writer.Allocating.init(allocator);
try fd.write(.{ .encoding = .plaintext, .allocator = allocator }, &buf.writer);
try testing.expectString("", buf.written());
}