Merge pull request #752 from lightpanda-io/url_search_params
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled

Rework/fix URLSearchParams
This commit is contained in:
Karl Seguin
2025-06-04 14:38:23 +08:00
committed by GitHub
4 changed files with 713 additions and 303 deletions

277
src/browser/key_value.zig Normal file
View File

@@ -0,0 +1,277 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
// Used by FormDAta and URLSearchParams.
//
// We store the values in an ArrayList rather than a an
// StringArrayHashMap([]const u8) because of the way the iterators (i.e., keys(),
// values() and entries()) work. The FormData can contain duplicate keys, and
// each iteration yields 1 key=>value pair. So, given:
//
// let f = new FormData();
// f.append('a', '1');
// f.append('a', '2');
//
// Then we'd expect f.keys(), f.values() and f.entries() to yield 2 results:
// ['a', '1']
// ['a', '2']
//
// This is much easier to do with an ArrayList than a HashMap, especially given
// that the FormData could be mutated while iterating.
// The downside is that most of the normal operations are O(N).
pub const List = struct {
entries: std.ArrayListUnmanaged(KeyValue) = .{},
pub fn init(entries: std.ArrayListUnmanaged(KeyValue)) List {
return .{ .entries = entries };
}
pub fn clone(self: *const List, arena: Allocator) !List {
const entries = self.entries.items;
var c: std.ArrayListUnmanaged(KeyValue) = .{};
try c.ensureTotalCapacity(arena, entries.len);
for (entries) |kv| {
c.appendAssumeCapacity(kv);
}
return .{ .entries = c };
}
pub fn fromOwnedSlice(entries: []KeyValue) List {
return .{
.entries = std.ArrayListUnmanaged(KeyValue).fromOwnedSlice(entries),
};
}
pub fn count(self: *const List) usize {
return self.entries.items.len;
}
pub fn get(self: *const List, key: []const u8) ?[]const u8 {
const result = self.find(key) orelse return null;
return result.entry.value;
}
pub fn getAll(self: *const List, arena: Allocator, key: []const u8) ![]const []const u8 {
var arr: std.ArrayListUnmanaged([]const u8) = .empty;
for (self.entries.items) |entry| {
if (std.mem.eql(u8, key, entry.key)) {
try arr.append(arena, entry.value);
}
}
return arr.items;
}
pub fn has(self: *const List, key: []const u8) bool {
return self.find(key) != null;
}
pub fn set(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void {
self.delete(key);
return self.append(arena, key, value);
}
pub fn append(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void {
return self.appendOwned(arena, try arena.dupe(u8, key), try arena.dupe(u8, value));
}
pub fn appendOwned(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void {
return self.entries.append(arena, .{
.key = key,
.value = value,
});
}
pub fn delete(self: *List, key: []const u8) void {
var i: usize = 0;
while (i < self.entries.items.len) {
const entry = self.entries.items[i];
if (std.mem.eql(u8, key, entry.key)) {
_ = self.entries.swapRemove(i);
} else {
i += 1;
}
}
}
pub fn deleteKeyValue(self: *List, key: []const u8, value: []const u8) void {
var i: usize = 0;
while (i < self.entries.items.len) {
const entry = self.entries.items[i];
if (std.mem.eql(u8, key, entry.key) and std.mem.eql(u8, value, entry.value)) {
_ = self.entries.swapRemove(i);
} else {
i += 1;
}
}
}
pub fn keyIterator(self: *const List) KeyIterator {
return .{ .entries = &self.entries };
}
pub fn valueIterator(self: *const List) ValueIterator {
return .{ .entries = &self.entries };
}
pub fn entryIterator(self: *const List) EntryIterator {
return .{ .entries = &self.entries };
}
pub fn ensureTotalCapacity(self: *List, arena: Allocator, len: usize) !void {
return self.entries.ensureTotalCapacity(arena, len);
}
const FindResult = struct {
index: usize,
entry: KeyValue,
};
fn find(self: *const List, key: []const u8) ?FindResult {
for (self.entries.items, 0..) |entry, i| {
if (std.mem.eql(u8, key, entry.key)) {
return .{ .index = i, .entry = entry };
}
}
return null;
}
};
pub const KeyValue = struct {
key: []const u8,
value: []const u8,
};
pub const KeyIterator = struct {
index: usize = 0,
entries: *const std.ArrayListUnmanaged(KeyValue),
pub fn _next(self: *KeyIterator) ?[]const u8 {
const entries = self.entries.items;
const index = self.index;
if (index == entries.len) {
return null;
}
self.index += 1;
return entries[index].key;
}
};
pub const ValueIterator = struct {
index: usize = 0,
entries: *const std.ArrayListUnmanaged(KeyValue),
pub fn _next(self: *ValueIterator) ?[]const u8 {
const entries = self.entries.items;
const index = self.index;
if (index == entries.len) {
return null;
}
self.index += 1;
return entries[index].value;
}
};
pub const EntryIterator = struct {
index: usize = 0,
entries: *const std.ArrayListUnmanaged(KeyValue),
pub fn _next(self: *EntryIterator) ?struct { []const u8, []const u8 } {
const entries = self.entries.items;
const index = self.index;
if (index == entries.len) {
return null;
}
self.index += 1;
const entry = entries[index];
return .{ entry.key, entry.value };
}
};
const URLEncodeMode = enum {
form,
query,
};
pub fn urlEncode(list: List, mode: URLEncodeMode, writer: anytype) !void {
const entries = list.entries.items;
if (entries.len == 0) {
return;
}
try urlEncodeEntry(entries[0], mode, writer);
for (entries[1..]) |entry| {
try writer.writeByte('&');
try urlEncodeEntry(entry, mode, writer);
}
}
fn urlEncodeEntry(entry: KeyValue, mode: URLEncodeMode, writer: anytype) !void {
try urlEncodeValue(entry.key, mode, writer);
// for a form, for an empty value, we'll do "spice="
// but for a query, we do "spice"
if (mode == .query and entry.value.len == 0) {
return;
}
try writer.writeByte('=');
try urlEncodeValue(entry.value, mode, writer);
}
fn urlEncodeValue(value: []const u8, mode: URLEncodeMode, writer: anytype) !void {
if (!urlEncodeShouldEscape(value, mode)) {
return writer.writeAll(value);
}
for (value) |b| {
if (urlEncodeUnreserved(b, mode)) {
try writer.writeByte(b);
} else if (b == ' ' and mode == .form) {
// for form submission, space should be encoded as '+', not '%20'
try writer.writeByte('+');
} else {
try writer.print("%{X:0>2}", .{b});
}
}
}
fn urlEncodeShouldEscape(value: []const u8, mode: URLEncodeMode) bool {
for (value) |b| {
if (!urlEncodeUnreserved(b, mode)) {
return true;
}
}
return false;
}
fn urlEncodeUnreserved(b: u8, mode: URLEncodeMode) bool {
return switch (b) {
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_' => true,
'~' => mode == .query,
else => false,
};
}

View File

@@ -18,56 +18,46 @@
const std = @import("std"); const std = @import("std");
const Reader = @import("../../str/parser.zig").Reader; const Allocator = std.mem.Allocator;
const asUint = @import("../../str/parser.zig").asUint;
// Values is a map with string key of string values. // Values is a map with string key of string values.
pub const Values = struct { pub const Values = struct {
arena: std.heap.ArenaAllocator, map: std.StringArrayHashMapUnmanaged(List) = .{},
map: std.StringArrayHashMapUnmanaged(List),
const List = std.ArrayListUnmanaged([]const u8); const List = std.ArrayListUnmanaged([]const u8);
pub fn init(allocator: std.mem.Allocator) Values {
return .{
.map = .{},
.arena = std.heap.ArenaAllocator.init(allocator),
};
}
pub fn deinit(self: *Values) void {
self.arena.deinit();
}
// add the key value couple to the values. // add the key value couple to the values.
// the key and the value are duplicated. // the key and the value are duplicated.
pub fn append(self: *Values, k: []const u8, v: []const u8) !void { pub fn append(self: *Values, arena: Allocator, k: []const u8, v: []const u8) !void {
const allocator = self.arena.allocator(); const owned_value = try arena.dupe(u8, v);
var gop = try self.map.getOrPut(arena, k);
errdefer _ = self.map.orderedRemove(k);
if (gop.found_existing) {
return gop.value_ptr.append(arena, owned_value);
}
gop.key_ptr.* = try arena.dupe(u8, k);
var list = List{};
try list.append(arena, owned_value);
gop.value_ptr.* = list;
}
pub fn set(self: *Values, arena: Allocator, k: []const u8, v: []const u8) !void {
const owned_value = try allocator.dupe(u8, v); const owned_value = try allocator.dupe(u8, v);
var gop = try self.map.getOrPut(allocator, k); var gop = try self.map.getOrPut(allocator, k);
errdefer _ = self.map.remove(k);
if (gop.found_existing) { if (gop.found_existing) {
return gop.value_ptr.append(allocator, owned_value); gop.value_ptr.clearRetainingCapacity();
} else {
gop._key_ptr.* = try arena.dupe(u8, k);
gop.value_ptr.* = .empty;
} }
try gop.value_ptr.append(arena, owned_value);
gop.key_ptr.* = try allocator.dupe(u8, k);
var list = List{};
try list.append(allocator, owned_value);
gop.value_ptr.* = list;
}
// append by taking the ownership of the key and the value
fn appendOwned(self: *Values, k: []const u8, v: []const u8) !void {
const allocator = self.arena.allocator();
var gop = try self.map.getOrPut(allocator, k);
if (gop.found_existing) {
return gop.value_ptr.append(allocator, v);
}
var list = List{};
try list.append(allocator, v);
gop.value_ptr.* = list;
} }
pub fn get(self: *const Values, k: []const u8) []const []const u8 { pub fn get(self: *const Values, k: []const u8) []const []const u8 {
@@ -80,13 +70,16 @@ pub const Values = struct {
pub fn first(self: *const Values, k: []const u8) []const u8 { pub fn first(self: *const Values, k: []const u8) []const u8 {
if (self.map.getPtr(k)) |list| { if (self.map.getPtr(k)) |list| {
if (list.items.len == 0) return ""; std.debug.assert(liste.items.len > 0);
return list.items[0]; return list.items[0];
} }
return ""; return "";
} }
pub fn has(self: *const Values, k: []const u8) bool {
return self.map.contains(k);
}
pub fn delete(self: *Values, k: []const u8) void { pub fn delete(self: *Values, k: []const u8) void {
_ = self.map.fetchSwapRemove(k); _ = self.map.fetchSwapRemove(k);
} }
@@ -97,6 +90,9 @@ pub const Values = struct {
for (list.items, 0..) |vv, i| { for (list.items, 0..) |vv, i| {
if (std.mem.eql(u8, v, vv)) { if (std.mem.eql(u8, v, vv)) {
_ = list.swapRemove(i); _ = list.swapRemove(i);
if (i == 0) {
_ = self.map.orderedRemove(k);
}
return; return;
} }
} }
@@ -252,27 +248,27 @@ fn unescape(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
const encoded = input[input_pos + 1 .. input_pos + 3]; const encoded = input[input_pos + 1 .. input_pos + 3];
const encoded_as_uint = @as(u16, @bitCast(encoded[0..2].*)); const encoded_as_uint = @as(u16, @bitCast(encoded[0..2].*));
unescaped[unescaped_pos] = switch (encoded_as_uint) { unescaped[unescaped_pos] = switch (encoded_as_uint) {
asUint("20") => ' ', asUint(u16, "20") => ' ',
asUint("21") => '!', asUint(u16, "21") => '!',
asUint("22") => '"', asUint(u16, "22") => '"',
asUint("23") => '#', asUint(u16, "23") => '#',
asUint("24") => '$', asUint(u16, "24") => '$',
asUint("25") => '%', asUint(u16, "25") => '%',
asUint("26") => '&', asUint(u16, "26") => '&',
asUint("27") => '\'', asUint(u16, "27") => '\'',
asUint("28") => '(', asUint(u16, "28") => '(',
asUint("29") => ')', asUint(u16, "29") => ')',
asUint("2A") => '*', asUint(u16, "2A") => '*',
asUint("2B") => '+', asUint(u16, "2B") => '+',
asUint("2C") => ',', asUint(u16, "2C") => ',',
asUint("2F") => '/', asUint(u16, "2F") => '/',
asUint("3A") => ':', asUint(u16, "3A") => ':',
asUint("3B") => ';', asUint(u16, "3B") => ';',
asUint("3D") => '=', asUint(u16, "3D") => '=',
asUint("3F") => '?', asUint(u16, "3F") => '?',
asUint("40") => '@', asUint(u16, "40") => '@',
asUint("5B") => '[', asUint(u16, "5B") => '[',
asUint("5D") => ']', asUint(u16, "5D") => ']',
else => HEX_DECODE[encoded[0]] << 4 | HEX_DECODE[encoded[1]], else => HEX_DECODE[encoded[0]] << 4 | HEX_DECODE[encoded[1]],
}; };
input_pos += 3; input_pos += 3;
@@ -286,7 +282,11 @@ fn unescape(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
return unescaped; return unescaped;
} }
const testing = std.testing; pub fn asUint(comptime T: type, comptime string: []const u8) T {
return @bitCast(string[0..string.len].*);
}
const testing = @import("../../testing.zig");
test "url.Query: unescape" { test "url.Query: unescape" {
const allocator = testing.allocator; const allocator = testing.allocator;
const cases = [_]struct { expected: []const u8, input: []const u8, free: bool }{ const cases = [_]struct { expected: []const u8, input: []const u8, free: bool }{
@@ -304,7 +304,7 @@ test "url.Query: unescape" {
defer if (case.free) { defer if (case.free) {
allocator.free(value); allocator.free(value);
}; };
try testing.expectEqualStrings(case.expected, value); try testing.expectEqual(case.expected, value);
} }
try testing.expectError(error.EscapeError, unescape(undefined, "%")); try testing.expectError(error.EscapeError, unescape(undefined, "%"));
@@ -346,21 +346,22 @@ test "url.Query: parseQuery" {
} }
test "url.Query.Values: get/first/count" { test "url.Query.Values: get/first/count" {
var values = Values.init(testing.allocator); defer testing.reset();
defer values.deinit(); const arena = testing.arena_allocator;
var values = Values{};
{ {
// empty // empty
try testing.expectEqual(0, values.count()); try testing.expectEqual(0, values.count());
try testing.expectEqual(0, values.get("").len); try testing.expectEqual(0, values.get("").len);
try testing.expectEqualStrings("", values.first("")); try testing.expectEqual("", values.first(""));
try testing.expectEqual(0, values.get("key").len); try testing.expectEqual(0, values.get("key").len);
try testing.expectEqualStrings("", values.first("key")); try testing.expectEqual("", values.first("key"));
} }
{ {
// add 1 value => key // add 1 value => key
try values.appendOwned("key", "value"); try values.append(arena, "key", "value");
try testing.expectEqual(1, values.count()); try testing.expectEqual(1, values.count());
try testing.expectEqual(1, values.get("key").len); try testing.expectEqual(1, values.get("key").len);
try testing.expectEqualSlices( try testing.expectEqualSlices(
@@ -368,12 +369,12 @@ test "url.Query.Values: get/first/count" {
&.{"value"}, &.{"value"},
values.get("key"), values.get("key"),
); );
try testing.expectEqualStrings("value", values.first("key")); try testing.expectEqual("value", values.first("key"));
} }
{ {
// add another value for the same key // add another value for the same key
try values.appendOwned("key", "another"); try values.append(arena, "key", "another");
try testing.expectEqual(1, values.count()); try testing.expectEqual(1, values.count());
try testing.expectEqual(2, values.get("key").len); try testing.expectEqual(2, values.get("key").len);
try testing.expectEqualSlices( try testing.expectEqualSlices(
@@ -381,12 +382,12 @@ test "url.Query.Values: get/first/count" {
&.{ "value", "another" }, &.{ "value", "another" },
values.get("key"), values.get("key"),
); );
try testing.expectEqualStrings("value", values.first("key")); try testing.expectEqual("value", values.first("key"));
} }
{ {
// add a new key (and value) // add a new key (and value)
try values.appendOwned("over", "9000!"); try values.append(arena, "over", "9000!");
try testing.expectEqual(2, values.count()); try testing.expectEqual(2, values.count());
try testing.expectEqual(2, values.get("key").len); try testing.expectEqual(2, values.get("key").len);
try testing.expectEqual(1, values.get("over").len); try testing.expectEqual(1, values.get("over").len);
@@ -395,7 +396,20 @@ test "url.Query.Values: get/first/count" {
&.{"9000!"}, &.{"9000!"},
values.get("over"), values.get("over"),
); );
try testing.expectEqualStrings("9000!", values.first("over")); try testing.expectEqual("9000!", values.first("over"));
}
{
// set (override)
try values.append(arena, "key", "9000!");
try testing.expectEqual(1, values.count());
try testing.expectEqual(1, values.get("key").len);
try testing.expectEqualSlices(
[]const u8,
&.{"9000!"},
values.get("key"),
);
try testing.expectEqual("9000!", values.first("key"));
} }
} }
@@ -409,7 +423,7 @@ test "url.Query.Values: encode" {
var buf: std.ArrayListUnmanaged(u8) = .{}; var buf: std.ArrayListUnmanaged(u8) = .{};
defer buf.deinit(testing.allocator); defer buf.deinit(testing.allocator);
try values.encode(buf.writer(testing.allocator)); try values.encode(buf.writer(testing.allocator));
try testing.expectEqualStrings( try testing.expectEqual(
"hello=world&i%20will%20not%20fear=%3E%3E&a=b&a=c", "hello=world&i%20will%20not%20fear=%3E%3E&a=b&a=c",
buf.items, buf.items,
); );
@@ -425,7 +439,7 @@ fn testParseQuery(expected: anytype, query: []const u8) !void {
const expect = @field(expected, f.name); const expect = @field(expected, f.name);
try testing.expectEqual(expect.len, actual.len); try testing.expectEqual(expect.len, actual.len);
for (expect, actual) |e, a| { for (expect, actual) |e, a| {
try testing.expectEqualStrings(e, a); try testing.expectEqual(e, a);
} }
count += 1; count += 1;
} }

View File

@@ -17,13 +17,20 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Page = @import("../page.zig").Page; const Allocator = std.mem.Allocator;
const query = @import("query.zig"); const Page = @import("../page.zig").Page;
const FormData = @import("../xhr/form_data.zig").FormData;
const kv = @import("../key_value.zig");
const iterator = @import("../iterator/iterator.zig");
pub const Interfaces = .{ pub const Interfaces = .{
URL, URL,
URLSearchParams, URLSearchParams,
KeyIterable,
ValueIterable,
EntryIterable,
}; };
// https://url.spec.whatwg.org/#url // https://url.spec.whatwg.org/#url
@@ -61,7 +68,7 @@ pub const URL = struct {
return init(arena, uri); return init(arena, uri);
} }
pub fn init(arena: std.mem.Allocator, uri: std.Uri) !URL { pub fn init(arena: Allocator, uri: std.Uri) !URL {
return .{ return .{
.uri = uri, .uri = uri,
.search_params = try URLSearchParams.init( .search_params = try URLSearchParams.init(
@@ -93,15 +100,15 @@ pub const URL = struct {
const cur = self.uri.query; const cur = self.uri.query;
defer self.uri.query = cur; defer self.uri.query = cur;
var q = std.ArrayList(u8).init(arena); var q = std.ArrayList(u8).init(arena);
try self.search_params.values.encode(q.writer()); try self.search_params.encode(q.writer());
self.uri.query = .{ .percent_encoded = q.items }; self.uri.query = .{ .percent_encoded = q.items };
return try self.toString(arena); return try self.toString(arena);
} }
// format the url with all its components. // format the url with all its components.
pub fn toString(self: *URL, arena: std.mem.Allocator) ![]const u8 { pub fn toString(self: *URL, arena: Allocator) ![]const u8 {
var buf = std.ArrayList(u8).init(arena); var buf: std.ArrayListUnmanaged(u8) = .empty;
try self.uri.writeToStream(.{ try self.uri.writeToStream(.{
.scheme = true, .scheme = true,
@@ -110,7 +117,8 @@ pub const URL = struct {
.path = uriComponentNullStr(self.uri.path).len > 0, .path = uriComponentNullStr(self.uri.path).len > 0,
.query = uriComponentNullStr(self.uri.query).len > 0, .query = uriComponentNullStr(self.uri.query).len > 0,
.fragment = uriComponentNullStr(self.uri.fragment).len > 0, .fragment = uriComponentNullStr(self.uri.fragment).len > 0,
}, buf.writer()); }, buf.writer(arena));
return buf.items; return buf.items;
} }
@@ -165,7 +173,7 @@ pub const URL = struct {
var buf: std.ArrayListUnmanaged(u8) = .{}; var buf: std.ArrayListUnmanaged(u8) = .{};
try buf.append(arena, '?'); try buf.append(arena, '?');
try self.search_params.values.encode(buf.writer(arena)); try self.search_params.encode(buf.writer(arena));
return buf.items; return buf.items;
} }
@@ -201,47 +209,227 @@ fn uriComponentStr(c: std.Uri.Component) []const u8 {
} }
// https://url.spec.whatwg.org/#interface-urlsearchparams // https://url.spec.whatwg.org/#interface-urlsearchparams
// TODO array like
pub const URLSearchParams = struct { pub const URLSearchParams = struct {
values: query.Values, entries: kv.List,
pub fn constructor(qs: ?[]const u8, page: *Page) !URLSearchParams { const URLSearchParamsOpts = union(enum) {
return init(page.arena, qs); qs: []const u8,
} form_data: *const FormData,
};
pub fn init(arena: std.mem.Allocator, qs: ?[]const u8) !URLSearchParams { pub fn constructor(opts_: ?URLSearchParamsOpts, page: *Page) !URLSearchParams {
return .{ const opts = opts_ orelse return .{ .entries = .{} };
.values = try query.parseQuery(arena, qs orelse ""), return switch (opts) {
.qs => |qs| init(page.arena, qs),
.form_data => |fd| .{ .entries = try fd.entries.clone(page.arena) },
}; };
} }
pub fn get_size(self: *URLSearchParams) u32 { pub fn init(arena: Allocator, qs_: ?[]const u8) !URLSearchParams {
return @intCast(self.values.count()); return .{
.entries = if (qs_) |qs| try parseQuery(arena, qs) else .{},
};
} }
pub fn _append(self: *URLSearchParams, name: []const u8, value: []const u8) !void { pub fn get_size(self: *const URLSearchParams) u32 {
try self.values.append(name, value); return @intCast(self.entries.count());
} }
pub fn _delete(self: *URLSearchParams, name: []const u8, value: ?[]const u8) !void { pub fn _append(self: *URLSearchParams, name: []const u8, value: []const u8, page: *Page) !void {
if (value) |v| return self.values.deleteValue(name, v); return self.entries.append(page.arena, name, value);
self.values.delete(name);
} }
pub fn _get(self: *URLSearchParams, name: []const u8) ?[]const u8 { pub fn _set(self: *URLSearchParams, name: []const u8, value: []const u8, page: *Page) !void {
return self.values.first(name); return self.entries.set(page.arena, name, value);
} }
// TODO return generates an error: caught unexpected error 'TypeLookup' pub fn _delete(self: *URLSearchParams, name: []const u8, value_: ?[]const u8) void {
// pub fn _getAll(self: *URLSearchParams, name: []const u8) [][]const u8 { if (value_) |value| {
// try self.values.get(name); return self.entries.deleteKeyValue(name, value);
// } }
return self.entries.delete(name);
}
pub fn _get(self: *const URLSearchParams, name: []const u8) ?[]const u8 {
return self.entries.get(name);
}
pub fn _getAll(self: *const URLSearchParams, name: []const u8, page: *Page) ![]const []const u8 {
return self.entries.getAll(page.call_arena, name);
}
pub fn _has(self: *const URLSearchParams, name: []const u8) bool {
return self.entries.has(name);
}
pub fn _keys(self: *const URLSearchParams) KeyIterable {
return .{ .inner = self.entries.keyIterator() };
}
pub fn _values(self: *const URLSearchParams) ValueIterable {
return .{ .inner = self.entries.valueIterator() };
}
pub fn _entries(self: *const URLSearchParams) EntryIterable {
return .{ .inner = self.entries.entryIterator() };
}
pub fn _symbol_iterator(self: *const URLSearchParams) EntryIterable {
return self._entries();
}
fn _toString(self: *const URLSearchParams, page: *Page) ![]const u8 {
var arr: std.ArrayListUnmanaged(u8) = .empty;
try kv.urlEncode(self.entries, .query, arr.writer(page.call_arena));
return arr.items;
}
// TODO // TODO
pub fn _sort(_: *URLSearchParams) void {} pub fn _sort(_: *URLSearchParams) void {}
fn encode(self: *const URLSearchParams, writer: anytype) !void {
return kv.urlEncode(self.entries, .query, writer);
}
}; };
// Parse the given query.
fn parseQuery(arena: Allocator, s: []const u8) !kv.List {
var list = kv.List{};
const ln = s.len;
if (ln == 0) {
return list;
}
var query = if (s[0] == '?') s[1..] else s;
while (query.len > 0) {
const i = std.mem.indexOfScalarPos(u8, query, 0, '=') orelse query.len;
const name = query[0..i];
var value: ?[]const u8 = null;
if (i < query.len) {
query = query[i + 1 ..];
const j = std.mem.indexOfScalarPos(u8, query, 0, '&') orelse query.len;
value = query[0..j];
query = if (j < query.len) query[j + 1 ..] else "";
} else {
query = "";
}
try list.appendOwned(
arena,
try unescape(arena, name),
if (value) |v| try unescape(arena, v) else "",
);
}
return list;
}
fn unescape(arena: Allocator, input: []const u8) ![]const u8 {
const HEX_CHAR = comptime blk: {
var all = std.mem.zeroes([256]bool);
for ('a'..('f' + 1)) |b| all[b] = true;
for ('A'..('F' + 1)) |b| all[b] = true;
for ('0'..('9' + 1)) |b| all[b] = true;
break :blk all;
};
const HEX_DECODE = comptime blk: {
var all = std.mem.zeroes([256]u8);
for ('a'..('z' + 1)) |b| all[b] = b - 'a' + 10;
for ('A'..('Z' + 1)) |b| all[b] = b - 'A' + 10;
for ('0'..('9' + 1)) |b| all[b] = b - '0';
break :blk all;
};
var has_plus = false;
var unescaped_len = input.len;
{
// Figure out if we have any spaces and what the final unescaped length
// will be (which will let us know if we have anything to unescape in
// the first place)
var i: usize = 0;
while (i < input.len) {
const c = input[i];
if (c == '%') {
if (i + 2 >= input.len or !HEX_CHAR[input[i + 1]] or !HEX_CHAR[input[i + 2]]) {
return error.EscapeError;
}
i += 3;
unescaped_len -= 2;
} else if (c == '+') {
has_plus = true;
i += 1;
} else {
i += 1;
}
}
}
// no encoding, and no plus. nothing to unescape
if (unescaped_len == input.len and has_plus == false) {
// we always dupe, because we know our caller wants it always duped.
return arena.dupe(u8, input);
}
var unescaped = try arena.alloc(u8, unescaped_len);
errdefer arena.free(unescaped);
var input_pos: usize = 0;
for (0..unescaped_len) |unescaped_pos| {
switch (input[input_pos]) {
'+' => {
unescaped[unescaped_pos] = ' ';
input_pos += 1;
},
'%' => {
const encoded = input[input_pos + 1 .. input_pos + 3];
const encoded_as_uint = @as(u16, @bitCast(encoded[0..2].*));
unescaped[unescaped_pos] = switch (encoded_as_uint) {
asUint(u16, "20") => ' ',
asUint(u16, "21") => '!',
asUint(u16, "22") => '"',
asUint(u16, "23") => '#',
asUint(u16, "24") => '$',
asUint(u16, "25") => '%',
asUint(u16, "26") => '&',
asUint(u16, "27") => '\'',
asUint(u16, "28") => '(',
asUint(u16, "29") => ')',
asUint(u16, "2A") => '*',
asUint(u16, "2B") => '+',
asUint(u16, "2C") => ',',
asUint(u16, "2F") => '/',
asUint(u16, "3A") => ':',
asUint(u16, "3B") => ';',
asUint(u16, "3D") => '=',
asUint(u16, "3F") => '?',
asUint(u16, "40") => '@',
asUint(u16, "5B") => '[',
asUint(u16, "5D") => ']',
else => HEX_DECODE[encoded[0]] << 4 | HEX_DECODE[encoded[1]],
};
input_pos += 3;
},
else => |c| {
unescaped[unescaped_pos] = c;
input_pos += 1;
},
}
}
return unescaped;
}
fn asUint(comptime T: type, comptime string: []const u8) T {
return @bitCast(string[0..string.len].*);
}
const KeyIterable = iterator.Iterable(kv.KeyIterator, "URLSearchParamsKeyIterator");
const ValueIterable = iterator.Iterable(kv.ValueIterator, "URLSearchParamsValueIterator");
const EntryIterable = iterator.Iterable(kv.EntryIterator, "URLSearchParamsEntryIterator");
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser.URL" { test "Browser.URL" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{}); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
@@ -269,17 +457,19 @@ test "Browser.URL" {
.{ "url.searchParams.get('b')", "~" }, .{ "url.searchParams.get('b')", "~" },
.{ "url.searchParams.append('c', 'foo')", "undefined" }, .{ "url.searchParams.append('c', 'foo')", "undefined" },
.{ "url.searchParams.get('c')", "foo" }, .{ "url.searchParams.get('c')", "foo" },
.{ "url.searchParams.getAll('c').length", "1" },
.{ "url.searchParams.getAll('c')[0]", "foo" },
.{ "url.searchParams.size", "3" }, .{ "url.searchParams.size", "3" },
// search is dynamic // search is dynamic
.{ "url.search", "?a=%7E&b=%7E&c=foo" }, .{ "url.search", "?a=~&b=~&c=foo" },
// href is dynamic // href is dynamic
.{ "url.href", "https://foo.bar/path?a=%7E&b=%7E&c=foo#fragment" }, .{ "url.href", "https://foo.bar/path?a=~&b=~&c=foo#fragment" },
.{ "url.searchParams.delete('c', 'foo')", "undefined" }, .{ "url.searchParams.delete('c', 'foo')", "undefined" },
.{ "url.searchParams.get('c')", "" }, .{ "url.searchParams.get('c')", "null" },
.{ "url.searchParams.delete('a')", "undefined" }, .{ "url.searchParams.delete('a')", "undefined" },
.{ "url.searchParams.get('a')", "" }, .{ "url.searchParams.get('a')", "null" },
}, .{}); }, .{});
try runner.testCases(&.{ try runner.testCases(&.{
@@ -287,3 +477,84 @@ test "Browser.URL" {
.{ "url.href", "https://lightpanda.io/over?9000" }, .{ "url.href", "https://lightpanda.io/over?9000" },
}, .{}); }, .{});
} }
test "Browser.URLSearchParams" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let usp = new URLSearchParams()", null },
.{ "usp.get('a')", "null" },
.{ "usp.has('a')", "false" },
.{ "usp.getAll('a')", "" },
.{ "usp.delete('a')", "undefined" },
.{ "usp.set('a', 1)", "undefined" },
.{ "usp.has('a')", "true" },
.{ "usp.get('a')", "1" },
.{ "usp.getAll('a')", "1" },
.{ "usp.append('a', 2)", "undefined" },
.{ "usp.has('a')", "true" },
.{ "usp.get('a')", "1" },
.{ "usp.getAll('a')", "1,2" },
.{ "usp.append('b', '3')", "undefined" },
.{ "usp.has('a')", "true" },
.{ "usp.get('a')", "1" },
.{ "usp.getAll('a')", "1,2" },
.{ "usp.has('b')", "true" },
.{ "usp.get('b')", "3" },
.{ "usp.getAll('b')", "3" },
.{ "let acc = [];", null },
.{ "for (const key of usp.keys()) { acc.push(key) }; acc;", "a,a,b" },
.{ "acc = [];", null },
.{ "for (const value of usp.values()) { acc.push(value) }; acc;", "1,2,3" },
.{ "acc = [];", null },
.{ "for (const entry of usp.entries()) { acc.push(entry) }; acc;", "a,1,a,2,b,3" },
.{ "acc = [];", null },
.{ "for (const entry of usp) { acc.push(entry) }; acc;", "a,1,a,2,b,3" },
.{ "usp.delete('a')", "undefined" },
.{ "usp.has('a')", "false" },
.{ "usp.has('b')", "true" },
.{ "acc = [];", null },
.{ "for (const key of usp.keys()) { acc.push(key) }; acc;", "b" },
.{ "acc = [];", null },
.{ "for (const value of usp.values()) { acc.push(value) }; acc;", "3" },
.{ "acc = [];", null },
.{ "for (const entry of usp.entries()) { acc.push(entry) }; acc;", "b,3" },
.{ "acc = [];", null },
.{ "for (const entry of usp) { acc.push(entry) }; acc;", "b,3" },
}, .{});
try runner.testCases(&.{
.{ "usp = new URLSearchParams('?hello')", null },
.{ "usp.get('hello')", "" },
.{ "usp = new URLSearchParams('?abc=')", null },
.{ "usp.get('abc')", "" },
.{ "usp = new URLSearchParams('?abc=123&')", null },
.{ "usp.get('abc')", "123" },
.{ "usp.size", "1" },
.{ "var fd = new FormData()", null },
.{ "fd.append('a', '1')", null },
.{ "fd.append('a', '2')", null },
.{ "fd.append('b', '3')", null },
.{ "ups = new URLSearchParams(fd)", null },
.{ "ups.size", "3" },
.{ "ups.getAll('a')", "1,2" },
.{ "ups.getAll('b')", "3" },
.{ "fd.delete('a')", null }, // the two aren't linked, it created a copy
.{ "ups.size", "3" },
}, .{});
}

View File

@@ -22,8 +22,9 @@ const Allocator = std.mem.Allocator;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const iterator = @import("../iterator/iterator.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const kv = @import("../key_value.zig");
const iterator = @import("../iterator/iterator.zig");
pub const Interfaces = .{ pub const Interfaces = .{
FormData, FormData,
@@ -32,29 +33,12 @@ pub const Interfaces = .{
EntryIterable, EntryIterable,
}; };
// We store the values in an ArrayList rather than a an
// StringArrayHashMap([]const u8) because of the way the iterators (i.e., keys(),
// values() and entries()) work. The FormData can contain duplicate keys, and
// each iteration yields 1 key=>value pair. So, given:
//
// let f = new FormData();
// f.append('a', '1');
// f.append('a', '2');
//
// Then we'd expect f.keys(), f.values() and f.entries() to yield 2 results:
// ['a', '1']
// ['a', '2']
//
// This is much easier to do with an ArrayList than a HashMap, especially given
// that the FormData could be mutated while iterating.
// The downside is that most of the normal operations are O(N).
// https://xhr.spec.whatwg.org/#interface-formdata // https://xhr.spec.whatwg.org/#interface-formdata
pub const FormData = struct { pub const FormData = struct {
entries: Entry.List, entries: kv.List,
pub fn constructor(form_: ?*parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !FormData { pub fn constructor(form_: ?*parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !FormData {
const form = form_ orelse return .{ .entries = .empty }; const form = form_ orelse return .{ .entries = .{} };
return fromForm(form, submitter_, page); return fromForm(form, submitter_, page);
} }
@@ -64,208 +48,77 @@ pub const FormData = struct {
} }
pub fn _get(self: *const FormData, key: []const u8) ?[]const u8 { pub fn _get(self: *const FormData, key: []const u8) ?[]const u8 {
const result = self.find(key) orelse return null; return self.entries.get(key);
return result.entry.value;
} }
pub fn _getAll(self: *const FormData, key: []const u8, page: *Page) ![][]const u8 { pub fn _getAll(self: *const FormData, key: []const u8, page: *Page) ![]const []const u8 {
const arena = page.call_arena; return self.entries.getAll(page.call_arena, key);
var arr: std.ArrayListUnmanaged([]const u8) = .empty;
for (self.entries.items) |entry| {
if (std.mem.eql(u8, key, entry.key)) {
try arr.append(arena, entry.value);
}
}
return arr.items;
} }
pub fn _has(self: *const FormData, key: []const u8) bool { pub fn _has(self: *const FormData, key: []const u8) bool {
return self.find(key) != null; return self.entries.has(key);
} }
// TODO: value should be a string or blog // TODO: value should be a string or blog
// TODO: another optional parameter for the filename // TODO: another optional parameter for the filename
pub fn _set(self: *FormData, key: []const u8, value: []const u8, page: *Page) !void { pub fn _set(self: *FormData, key: []const u8, value: []const u8, page: *Page) !void {
self._delete(key); return self.entries.set(page.arena, key, value);
return self._append(key, value, page);
} }
// TODO: value should be a string or blog // TODO: value should be a string or blog
// TODO: another optional parameter for the filename // TODO: another optional parameter for the filename
pub fn _append(self: *FormData, key: []const u8, value: []const u8, page: *Page) !void { pub fn _append(self: *FormData, key: []const u8, value: []const u8, page: *Page) !void {
const arena = page.arena; return self.entries.append(page.arena, key, value);
return self.entries.append(arena, .{ .key = try arena.dupe(u8, key), .value = try arena.dupe(u8, value) });
} }
pub fn _delete(self: *FormData, key: []const u8) void { pub fn _delete(self: *FormData, key: []const u8) void {
var i: usize = 0; return self.entries.delete(key);
while (i < self.entries.items.len) {
const entry = self.entries.items[i];
if (std.mem.eql(u8, key, entry.key)) {
_ = self.entries.swapRemove(i);
} else {
i += 1;
}
}
} }
pub fn _keys(self: *const FormData) KeyIterable { pub fn _keys(self: *const FormData) KeyIterable {
return .{ .inner = .{ .entries = &self.entries } }; return .{ .inner = self.entries.keyIterator() };
} }
pub fn _values(self: *const FormData) ValueIterable { pub fn _values(self: *const FormData) ValueIterable {
return .{ .inner = .{ .entries = &self.entries } }; return .{ .inner = self.entries.valueIterator() };
} }
pub fn _entries(self: *const FormData) EntryIterable { pub fn _entries(self: *const FormData) EntryIterable {
return .{ .inner = .{ .entries = &self.entries } }; return .{ .inner = self.entries.entryIterator() };
} }
pub fn _symbol_iterator(self: *const FormData) EntryIterable { pub fn _symbol_iterator(self: *const FormData) EntryIterable {
return self._entries(); return self._entries();
} }
const FindResult = struct {
index: usize,
entry: Entry,
};
fn find(self: *const FormData, key: []const u8) ?FindResult {
for (self.entries.items, 0..) |entry, i| {
if (std.mem.eql(u8, key, entry.key)) {
return .{ .index = i, .entry = entry };
}
}
return null;
}
pub fn write(self: *const FormData, encoding_: ?[]const u8, writer: anytype) !void { pub fn write(self: *const FormData, encoding_: ?[]const u8, writer: anytype) !void {
const encoding = encoding_ orelse { const encoding = encoding_ orelse {
return urlEncode(self, writer); return kv.urlEncode(self.entries, .form, writer);
}; };
if (std.ascii.eqlIgnoreCase(encoding, "application/x-www-form-urlencoded")) { if (std.ascii.eqlIgnoreCase(encoding, "application/x-www-form-urlencoded")) {
return urlEncode(self, writer); return kv.urlEncode(self.entries, .form, writer);
} }
log.warn(.web_api, "not implemented", .{ .feature = "form data encoding", .encoding = encoding }); log.warn(.web_api, "not implemented", .{
.feature = "form data encoding",
.encoding = encoding,
});
return error.EncodingNotSupported; return error.EncodingNotSupported;
} }
}; };
fn urlEncode(data: *const FormData, writer: anytype) !void { const KeyIterable = iterator.Iterable(kv.KeyIterator, "FormDataKeyIterator");
const entries = data.entries.items; const ValueIterable = iterator.Iterable(kv.ValueIterator, "FormDataValueIterator");
if (entries.len == 0) { const EntryIterable = iterator.Iterable(kv.EntryIterator, "FormDataEntryIterator");
return;
}
try urlEncodeEntry(entries[0], writer);
for (entries[1..]) |entry| {
try writer.writeByte('&');
try urlEncodeEntry(entry, writer);
}
}
fn urlEncodeEntry(entry: Entry, writer: anytype) !void {
try urlEncodeValue(entry.key, writer);
try writer.writeByte('=');
try urlEncodeValue(entry.value, writer);
}
fn urlEncodeValue(value: []const u8, writer: anytype) !void {
if (!urlEncodeShouldEscape(value)) {
return writer.writeAll(value);
}
for (value) |b| {
if (urlEncodeUnreserved(b)) {
try writer.writeByte(b);
} else if (b == ' ') {
// for form submission, space should be encoded as '+', not '%20'
try writer.writeByte('+');
} else {
try writer.print("%{X:0>2}", .{b});
}
}
}
fn urlEncodeShouldEscape(value: []const u8) bool {
for (value) |b| {
if (!urlEncodeUnreserved(b)) {
return true;
}
}
return false;
}
fn urlEncodeUnreserved(b: u8) bool {
return switch (b) {
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
else => false,
};
}
const Entry = struct {
key: []const u8,
value: []const u8,
pub const List = std.ArrayListUnmanaged(Entry);
};
const KeyIterable = iterator.Iterable(KeyIterator, "FormDataKeyIterator");
const ValueIterable = iterator.Iterable(ValueIterator, "FormDataValueIterator");
const EntryIterable = iterator.Iterable(EntryIterator, "FormDataEntryIterator");
const KeyIterator = struct {
index: usize = 0,
entries: *const Entry.List,
pub fn _next(self: *KeyIterator) ?[]const u8 {
const index = self.index;
if (index == self.entries.items.len) {
return null;
}
self.index += 1;
return self.entries.items[index].key;
}
};
const ValueIterator = struct {
index: usize = 0,
entries: *const Entry.List,
pub fn _next(self: *ValueIterator) ?[]const u8 {
const index = self.index;
if (index == self.entries.items.len) {
return null;
}
self.index += 1;
return self.entries.items[index].value;
}
};
const EntryIterator = struct {
index: usize = 0,
entries: *const Entry.List,
pub fn _next(self: *EntryIterator) ?struct { []const u8, []const u8 } {
const index = self.index;
if (index == self.entries.items.len) {
return null;
}
self.index += 1;
const entry = self.entries.items[index];
return .{ entry.key, entry.value };
}
};
// TODO: handle disabled fieldsets // TODO: handle disabled fieldsets
fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !Entry.List { fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !kv.List {
const arena = page.arena; const arena = page.arena;
const collection = try parser.formGetCollection(form); const collection = try parser.formGetCollection(form);
const len = try parser.htmlCollectionGetLength(collection); const len = try parser.htmlCollectionGetLength(collection);
var entries: Entry.List = .empty; var entries: kv.List = .{};
try entries.ensureTotalCapacity(arena, len); try entries.ensureTotalCapacity(arena, len);
var submitter_included = false; var submitter_included = false;
@@ -288,15 +141,10 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
if (std.ascii.eqlIgnoreCase(tpe, "image")) { if (std.ascii.eqlIgnoreCase(tpe, "image")) {
if (submitter_name_) |submitter_name| { if (submitter_name_) |submitter_name| {
if (std.mem.eql(u8, submitter_name, name)) { if (std.mem.eql(u8, submitter_name, name)) {
try entries.append(arena, .{ const key_x = try std.fmt.allocPrint(arena, "{s}.x", .{name});
.key = try std.fmt.allocPrint(arena, "{s}.x", .{name}), const key_y = try std.fmt.allocPrint(arena, "{s}.y", .{name});
.value = "0", try entries.appendOwned(arena, key_x, "0");
}); try entries.appendOwned(arena, key_y, "0");
try entries.append(arena, .{
.key = try std.fmt.allocPrint(arena, "{s}.y", .{name}),
.value = "0",
});
submitter_included = true; submitter_included = true;
} }
} }
@@ -315,7 +163,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
submitter_included = true; submitter_included = true;
} }
const value = (try parser.elementGetAttribute(element, "value")) orelse ""; const value = (try parser.elementGetAttribute(element, "value")) orelse "";
try entries.append(arena, .{ .key = name, .value = value }); try entries.appendOwned(arena, name, value);
}, },
.select => { .select => {
const select: *parser.Select = @ptrCast(node); const select: *parser.Select = @ptrCast(node);
@@ -324,12 +172,12 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
.textarea => { .textarea => {
const textarea: *parser.TextArea = @ptrCast(node); const textarea: *parser.TextArea = @ptrCast(node);
const value = try parser.textareaGetValue(textarea); const value = try parser.textareaGetValue(textarea);
try entries.append(arena, .{ .key = name, .value = value }); try entries.appendOwned(arena, name, value);
}, },
.button => if (submitter_name_) |submitter_name| { .button => if (submitter_name_) |submitter_name| {
if (std.mem.eql(u8, submitter_name, name)) { if (std.mem.eql(u8, submitter_name, name)) {
const value = (try parser.elementGetAttribute(element, "value")) orelse ""; const value = (try parser.elementGetAttribute(element, "value")) orelse "";
try entries.append(arena, .{ .key = name, .value = value }); try entries.appendOwned(arena, name, value);
submitter_included = true; submitter_included = true;
} }
}, },
@@ -345,14 +193,14 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
// this can happen if the submitter is outside the form, but associated // this can happen if the submitter is outside the form, but associated
// with the form via a form=ID attribute // with the form via a form=ID attribute
const value = (try parser.elementGetAttribute(@ptrCast(submitter), "value")) orelse ""; const value = (try parser.elementGetAttribute(@ptrCast(submitter), "value")) orelse "";
try entries.append(arena, .{ .key = submitter_name_.?, .value = value }); try entries.appendOwned(arena, submitter_name_.?, value);
} }
} }
return entries; return entries;
} }
fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u8, entries: *Entry.List, page: *Page) !void { fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u8, entries: *kv.List, page: *Page) !void {
const HTMLSelectElement = @import("../html/select.zig").HTMLSelectElement; const HTMLSelectElement = @import("../html/select.zig").HTMLSelectElement;
// Go through the HTMLSelectElement because it has specific logic for handling // Go through the HTMLSelectElement because it has specific logic for handling
@@ -372,7 +220,7 @@ fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u
return; return;
} }
const value = try parser.optionGetValue(option); const value = try parser.optionGetValue(option);
return entries.append(arena, .{ .key = name, .value = value }); return entries.appendOwned(arena, name, value);
} }
const len = try parser.optionCollectionGetLength(options); const len = try parser.optionCollectionGetLength(options);
@@ -386,7 +234,7 @@ fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u
if (try parser.optionGetSelected(option)) { if (try parser.optionGetSelected(option)) {
const value = try parser.optionGetValue(option); const value = try parser.optionGetValue(option);
try entries.append(arena, .{ .key = name, .value = value }); try entries.appendOwned(arena, name, value);
} }
} }
} }
@@ -420,7 +268,7 @@ test "Browser.FormData" {
\\ <input id="is_disabled" disabled value="nope2"> \\ <input id="is_disabled" disabled value="nope2">
\\ \\
\\ <input name="txt-1" value="txt-1-v"> \\ <input name="txt-1" value="txt-1-v">
\\ <input name="txt-2" value="txt-2-v" type=password> \\ <input name="txt-2" value="txt-~-v" type=password>
\\ \\
\\ <input name="chk-3" value="chk-3-va" type=checkbox> \\ <input name="chk-3" value="chk-3-va" type=checkbox>
\\ <input name="chk-3" value="chk-3-vb" type=checkbox checked> \\ <input name="chk-3" value="chk-3-vb" type=checkbox checked>
@@ -518,7 +366,7 @@ test "Browser.FormData" {
\\ acc.slice(0, -1) \\ acc.slice(0, -1)
, ,
\\txt-1=txt-1-v \\txt-1=txt-1-v
\\txt-2=txt-2-v \\txt-2=txt-~-v
\\chk-3=chk-3-vb \\chk-3=chk-3-vb
\\chk-3=chk-3-vc \\chk-3=chk-3-vc
\\rdi-1=rdi-1-vc \\rdi-1=rdi-1-vc
@@ -539,7 +387,7 @@ test "Browser.FormData: urlEncode" {
defer arr.deinit(testing.allocator); defer arr.deinit(testing.allocator);
{ {
var fd = FormData{ .entries = .empty }; var fd = FormData{ .entries = .{} };
try testing.expectError(error.EncodingNotSupported, fd.write("unknown", arr.writer(testing.allocator))); try testing.expectError(error.EncodingNotSupported, fd.write("unknown", arr.writer(testing.allocator)));
try fd.write(null, arr.writer(testing.allocator)); try fd.write(null, arr.writer(testing.allocator));
@@ -550,12 +398,12 @@ test "Browser.FormData: urlEncode" {
} }
{ {
var fd = FormData{ .entries = Entry.List.fromOwnedSlice(@constCast(&[_]Entry{ var fd = FormData{ .entries = kv.List.fromOwnedSlice(@constCast(&[_]kv.KeyValue{
.{ .key = "a", .value = "1" }, .{ .key = "a", .value = "1" },
.{ .key = "it's over", .value = "9000 !!!" }, .{ .key = "it's over", .value = "9000 !!!" },
.{ .key = "emot", .value = "ok: ☺" }, .{ .key = "em~ot", .value = "ok: ☺" },
})) }; })) };
const expected = "a=1&it%27s+over=9000+%21%21%21&emot=ok%3A+%E2%98%BA"; const expected = "a=1&it%27s+over=9000+%21%21%21&em%7Eot=ok%3A+%E2%98%BA";
try fd.write(null, arr.writer(testing.allocator)); try fd.write(null, arr.writer(testing.allocator));
try testing.expectEqual(expected, arr.items); try testing.expectEqual(expected, arr.items);