mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 09:35:59 -04:00
Merge branch 'main' into agent
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user