mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
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
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:
277
src/browser/key_value.zig
Normal file
277
src/browser/key_value.zig
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
}, .{});
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user