@@ -51,21 +51,15 @@ pub const Interfaces = .{
5151
5252// https://xhr.spec.whatwg.org/#interface-formdata
5353pub const FormData = struct {
54- entries : std . ArrayListUnmanaged ( Entry ) ,
54+ entries : Entry.List ,
5555
5656 pub fn constructor (form_ : ? * parser.Form , submitter_ : ? * parser.ElementHTML , page : * Page ) ! FormData {
5757 const form = form_ orelse return .{ .entries = .empty };
58- return fromForm (form , submitter_ , page , .{} );
58+ return fromForm (form , submitter_ , page );
5959 }
6060
61- const FromFormOpts = struct {
62- // Uses the page.arena if null. This is needed for when we're handling
63- // form submission from the Page, and we want to capture the form within
64- // the session's transfer_arena.
65- allocator : ? Allocator = null ,
66- };
67- pub fn fromForm (form : * parser.Form , submitter_ : ? * parser.ElementHTML , page : * Page , opts : FromFormOpts ) ! FormData {
68- const entries = try collectForm (opts .allocator orelse page .arena , form , submitter_ , page );
61+ pub fn fromForm (form : * parser.Form , submitter_ : ? * parser.ElementHTML , page : * Page ) ! FormData {
62+ const entries = try collectForm (form , submitter_ , page );
6963 return .{ .entries = entries };
7064 }
7165
@@ -144,11 +138,78 @@ pub const FormData = struct {
144138 }
145139 return null ;
146140 }
141+
142+ pub fn write (self : * const FormData , encoding_ : ? []const u8 , writer : anytype ) ! void {
143+ const encoding = encoding_ orelse {
144+ return urlEncode (self , writer );
145+ };
146+
147+ if (std .ascii .eqlIgnoreCase (encoding , "application/x-www-form-urlencoded" )) {
148+ return urlEncode (self , writer );
149+ }
150+
151+ log .warn (.form_data , "encoding not supported" , .{ .encoding = encoding });
152+ return error .EncodingNotSupported ;
153+ }
147154};
148155
156+ fn urlEncode (data : * const FormData , writer : anytype ) ! void {
157+ const entries = data .entries .items ;
158+ if (entries .len == 0 ) {
159+ return ;
160+ }
161+
162+ try urlEncodeEntry (entries [0 ], writer );
163+ for (entries [1.. ]) | entry | {
164+ try writer .writeByte ('&' );
165+ try urlEncodeEntry (entry , writer );
166+ }
167+ }
168+
169+ fn urlEncodeEntry (entry : Entry , writer : anytype ) ! void {
170+ try urlEncodeValue (entry .key , writer );
171+ try writer .writeByte ('=' );
172+ try urlEncodeValue (entry .value , writer );
173+ }
174+
175+ fn urlEncodeValue (value : []const u8 , writer : anytype ) ! void {
176+ if (! urlEncodeShouldEscape (value )) {
177+ return writer .writeAll (value );
178+ }
179+
180+ for (value ) | b | {
181+ if (urlEncodeUnreserved (b )) {
182+ try writer .writeByte (b );
183+ } else if (b == ' ' ) {
184+ // for form submission, space should be encoded as '+', not '%20'
185+ try writer .writeByte ('+' );
186+ } else {
187+ try writer .print ("%{X:0>2}" , .{b });
188+ }
189+ }
190+ }
191+
192+ fn urlEncodeShouldEscape (value : []const u8 ) bool {
193+ for (value ) | b | {
194+ if (! urlEncodeUnreserved (b )) {
195+ return true ;
196+ }
197+ }
198+ return false ;
199+ }
200+
201+ fn urlEncodeUnreserved (b : u8 ) bool {
202+ return switch (b ) {
203+ 'A' ... 'Z' , 'a' ... 'z' , '0' ... '9' , '-' , '.' , '_' , '~' = > true ,
204+ else = > false ,
205+ };
206+ }
207+
149208const Entry = struct {
150209 key : []const u8 ,
151210 value : []const u8 ,
211+
212+ pub const List = std .ArrayListUnmanaged (Entry );
152213};
153214
154215const KeyIterable = iterator .Iterable (KeyIterator , "FormDataKeyIterator" );
@@ -157,7 +218,7 @@ const EntryIterable = iterator.Iterable(EntryIterator, "FormDataEntryIterator");
157218
158219const KeyIterator = struct {
159220 index : usize = 0 ,
160- entries : * const std . ArrayListUnmanaged ( Entry ) ,
221+ entries : * const Entry.List ,
161222
162223 pub fn _next (self : * KeyIterator ) ? []const u8 {
163224 const index = self .index ;
@@ -171,7 +232,7 @@ const KeyIterator = struct {
171232
172233const ValueIterator = struct {
173234 index : usize = 0 ,
174- entries : * const std . ArrayListUnmanaged ( Entry ) ,
235+ entries : * const Entry.List ,
175236
176237 pub fn _next (self : * ValueIterator ) ? []const u8 {
177238 const index = self .index ;
@@ -185,7 +246,7 @@ const ValueIterator = struct {
185246
186247const EntryIterator = struct {
187248 index : usize = 0 ,
188- entries : * const std . ArrayListUnmanaged ( Entry ) ,
249+ entries : * const Entry.List ,
189250
190251 pub fn _next (self : * EntryIterator ) ? struct { []const u8 , []const u8 } {
191252 const index = self .index ;
@@ -198,11 +259,13 @@ const EntryIterator = struct {
198259 }
199260};
200261
201- fn collectForm (arena : Allocator , form : * parser.Form , submitter_ : ? * parser.ElementHTML , page : * Page ) ! std. ArrayListUnmanaged (Entry ) {
262+ // TODO: handle disabled fieldsets
263+ fn collectForm (form : * parser.Form , submitter_ : ? * parser.ElementHTML , page : * Page ) ! Entry.List {
264+ const arena = page .arena ;
202265 const collection = try parser .formGetCollection (form );
203266 const len = try parser .htmlCollectionGetLength (collection );
204267
205- var entries : std . ArrayListUnmanaged ( Entry ) = .empty ;
268+ var entries : Entry.List = .empty ;
206269 try entries .ensureTotalCapacity (arena , len );
207270
208271 const submitter_name_ = try getSubmitterName (submitter_ );
@@ -275,7 +338,7 @@ fn collectForm(arena: Allocator, form: *parser.Form, submitter_: ?*parser.Elemen
275338 return entries ;
276339}
277340
278- fn collectSelectValues (arena : Allocator , select : * parser.Select , name : []const u8 , entries : * std . ArrayListUnmanaged ( Entry ) , page : * Page ) ! void {
341+ fn collectSelectValues (arena : Allocator , select : * parser.Select , name : []const u8 , entries : * Entry.List , page : * Page ) ! void {
279342 const HTMLSelectElement = @import ("../html/select.zig" ).HTMLSelectElement ;
280343
281344 // Go through the HTMLSelectElement because it has specific logic for handling
@@ -456,3 +519,34 @@ test "Browser.FormData" {
456519 },
457520 }, .{});
458521}
522+
523+ test "Browser.FormData: urlEncode" {
524+ var arr : std .ArrayListUnmanaged (u8 ) = .empty ;
525+ defer arr .deinit (testing .allocator );
526+
527+ {
528+ var fd = FormData { .entries = .empty };
529+ try testing .expectError (error .EncodingNotSupported , fd .write ("unknown" , arr .writer (testing .allocator )));
530+
531+ try fd .write (null , arr .writer (testing .allocator ));
532+ try testing .expectEqual ("" , arr .items );
533+
534+ try fd .write ("application/x-www-form-urlencoded" , arr .writer (testing .allocator ));
535+ try testing .expectEqual ("" , arr .items );
536+ }
537+
538+ {
539+ var fd = FormData { .entries = Entry .List .fromOwnedSlice (@constCast (&[_ ]Entry {
540+ .{ .key = "a" , .value = "1" },
541+ .{ .key = "it's over" , .value = "9000 !!!" },
542+ .{ .key = "emot" , .value = "ok: ☺" },
543+ })) };
544+ const expected = "a=1&it%27s+over=9000+%21%21%21&emot=ok%3A+%E2%98%BA" ;
545+ try fd .write (null , arr .writer (testing .allocator ));
546+ try testing .expectEqual (expected , arr .items );
547+
548+ arr .clearRetainingCapacity ();
549+ try fd .write ("application/x-www-form-urlencoded" , arr .writer (testing .allocator ));
550+ try testing .expectEqual (expected , arr .items );
551+ }
552+ }
0 commit comments