add FormData and base KeyValueList

This commit is contained in:
Karl Seguin
2025-10-31 22:25:19 +08:00
parent c966211481
commit 618b28a292
5 changed files with 325 additions and 87 deletions

View File

@@ -444,6 +444,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/css/CSSStyleProperties.zig"),
@import("../webapi/Document.zig"),
@import("../webapi/HTMLDocument.zig"),
@import("../webapi/KeyValueList.zig"),
@import("../webapi/DocumentFragment.zig"),
@import("../webapi/DOMException.zig"),
@import("../webapi/DOMTreeWalker.zig"),
@@ -489,6 +490,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/EventTarget.zig"),
@import("../webapi/Location.zig"),
@import("../webapi/Navigator.zig"),
@import("../webapi/net/FormData.zig"),
@import("../webapi/net/Request.zig"),
@import("../webapi/net/Response.zig"),
@import("../webapi/net/URLSearchParams.zig"),

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id=main></div>
<script id=webcomponents>
class LightPanda extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.append('connected');
}
}
window.customElements.define("lightpanda-test", LightPanda);
const main = document.getElementById('main');
main.appendChild(document.createElement('lightpanda-test'));
testing.expectEqual('<lightpanda-test>connected</lightpanda-test>', main.innerHTML)
testing.expectEqual('[object DataSet]', document.createElement('lightpanda-test').dataset.toString());
testing.expectEqual('[object DataSet]', document.createElement('lightpanda-test.x').dataset.toString());
</script>

View File

@@ -0,0 +1,146 @@
const std = @import("std");
const String = @import("../../string.zig").String;
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Allocator = std.mem.Allocator;
pub fn registerTypes() []const type {
return &.{
KeyIterator,
ValueIterator,
EntryIterator,
};
}
pub const KeyValueList = @This();
_entries: std.ArrayListUnmanaged(Entry) = .empty,
pub const empty: KeyValueList = .{
._entries = .empty,
};
pub const Entry = struct {
name: String,
value: String,
};
pub fn init() KeyValueList {
return .{};
}
pub fn ensureTotalCapacity(self: *KeyValueList, allocator: Allocator, n: usize) !void {
return self._entries.ensureTotalCapacity(allocator, n);
}
pub fn get(self: *const KeyValueList, name: []const u8) ?[]const u8 {
for (self._entries.items) |*entry| {
if (entry.name.eqlSlice(name)) {
return entry.value.str();
}
}
return null;
}
pub fn getAll(self: *const KeyValueList, name: []const u8, page: *Page) ![]const []const u8 {
const arena = page.call_arena;
var arr: std.ArrayList([]const u8) = .empty;
for (self._entries.items) |*entry| {
if (entry.name.eqlSlice(name)) {
try arr.append(arena, entry.value.str());
}
}
return arr.items;
}
pub fn has(self: *const KeyValueList, name: []const u8) bool {
for (self._entries.items) |*entry| {
if (entry.name.eqlSlice(name)) {
return true;
}
}
return false;
}
pub fn append(self: *KeyValueList, allocator: Allocator, name: []const u8, value: []const u8) !void {
try self._entries.append(allocator, .{
.name = try String.init(allocator, name, .{}),
.value = try String.init(allocator, value, .{}),
});
}
pub fn appendAssumeCapacity(self: *KeyValueList, allocator: Allocator, name: []const u8, value: []const u8) !void {
self._entries.appendAssumeCapacity(.{
.name = try String.init(allocator, name, .{}),
.value = try String.init(allocator, value, .{}),
});
}
pub fn delete(self: *KeyValueList, name: []const u8, value: ?[]const u8) void {
var i: usize = 0;
while (i < self._entries.items.len) {
const entry = self._entries.items[i];
if (entry.name.eqlSlice(name)) {
if (value == null or entry.value.eqlSlice(value.?)) {
_ = self._entries.swapRemove(i);
continue;
}
}
i += 1;
}
}
pub fn set(self: *KeyValueList, allocator: Allocator, name: []const u8, value: []const u8) !void {
self.delete(name, null);
try self.append(allocator, name, value);
}
pub fn len(self: *const KeyValueList) usize {
return self._entries.items.len;
}
pub fn items(self: *const KeyValueList) []const Entry {
return self._entries.items;
}
pub const Iterator = struct {
index: u32 = 0,
kv: *KeyValueList,
// Why? Because whenever an Iterator is created, we need to increment the
// RC of what it's iterating. And when the iterator is destroyed, we need
// to decrement it. The generic iterator which will wrap this handles that
// by using this "list" field. Most things that use the GenericIterator can
// just set `list: *ZigCollection`, and everything will work. But KeyValueList
// is being composed by various types, so it can't reference those types.
// Using *anyopaque here is "dangerous", in that it requires the composer
// to pass the right value, which normally would be itself (`*Self`), but
// only because (as of now) everyting that uses KeyValueList has no prototype
list: *anyopaque,
pub const Entry = struct { []const u8, []const u8 };
pub fn next(self: *Iterator, _: *const Page) ?Iterator.Entry {
const index = self.index;
const entries = self.kv._entries.items;
if (index >= entries.len) {
return null;
}
self.index = index + 1;
const e = &entries[index];
return .{ e.name.str(), e.value.str() };
}
};
pub fn iterator(self: *const KeyValueList) Iterator {
return .{ .list = self };
}
const GenericIterator = @import("collections/iterator.zig").Entry;
pub const KeyIterator = GenericIterator(Iterator, "0");
pub const ValueIterator = GenericIterator(Iterator, "1");
pub const EntryIterator = GenericIterator(Iterator, null);

View File

@@ -0,0 +1,115 @@
const std = @import("std");
const log = @import("../../../log.zig");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const KeyValueList = @import("../KeyValueList.zig");
const Alloctor = std.mem.Allocator;
const FormData = @This();
_arena: Alloctor,
_list: KeyValueList,
pub fn init(page: *Page) !*FormData {
return page._factory.create(FormData{
._arena = page.arena,
._list = KeyValueList.init(),
});
}
pub fn get(self: *const FormData, name: []const u8) ?[]const u8 {
return self._list.get(name);
}
pub fn getAll(self: *const FormData, name: []const u8, page: *Page) ![]const []const u8 {
return self._list.getAll(name, page);
}
pub fn has(self: *const FormData, name: []const u8) bool {
return self._list.has(name);
}
pub fn set(self: *FormData, name: []const u8, value: []const u8) !void {
return self._list.set(self._arena, name, value);
}
pub fn append(self: *FormData, name: []const u8, value: []const u8) !void {
return self._list.append(self._arena, name, value);
}
pub fn delete(self: *FormData, name: []const u8) void {
self._list.delete(name, null);
}
pub fn keys(self: *FormData, page: *Page) !*KeyValueList.KeyIterator {
return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._list }, page);
}
pub fn values(self: *FormData, page: *Page) !*KeyValueList.ValueIterator {
return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._list }, page);
}
pub fn entries(self: *FormData, page: *Page) !*KeyValueList.EntryIterator {
return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._list }, page);
}
pub fn forEach(self: *FormData, cb_: js.Function, js_this_: ?js.Object) !void {
const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;
for (self._list._entries.items) |entry| {
cb.call(void, .{ entry.value.str(), entry.name.str(), self }) catch |err| {
// this is a non-JS error
log.warn(.js, "FormData.forEach", .{ .err = err });
};
}
}
pub const Iterator = struct {
index: u32 = 0,
list: *const FormData,
const Entry = struct { []const u8, []const u8 };
pub fn next(self: *Iterator, _: *Page) !?Iterator.Entry {
const index = self.index;
const items = self.list._list.items();
if (index >= items.len) {
return null;
}
self.index = index + 1;
const e = &items[index];
return .{ e.name.str(), e.value.str() };
}
};
pub const JsApi = struct {
pub const bridge = js.Bridge(FormData);
pub const Meta = struct {
pub const name = "FormData";
pub const prototype_chain = bridge.prototypeChain();
pub var class_index: u16 = 0;
};
pub const constructor = bridge.constructor(FormData.init, .{});
pub const has = bridge.function(FormData.has, .{});
pub const get = bridge.function(FormData.get, .{});
pub const set = bridge.function(FormData.set, .{});
pub const append = bridge.function(FormData.append, .{});
pub const getAll = bridge.function(FormData.getAll, .{});
pub const delete = bridge.function(FormData.delete, .{});
pub const keys = bridge.function(FormData.keys, .{});
pub const values = bridge.function(FormData.values, .{});
pub const entries = bridge.function(FormData.entries, .{});
pub const symbol_iterator = bridge.iterator(FormData.entries, .{});
pub const forEach = bridge.function(FormData.forEach, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: FormData" {
try testing.htmlRunner("net/form_data.html", .{});
}

View File

@@ -7,24 +7,12 @@ const Allocator = std.mem.Allocator;
const Page = @import("../../Page.zig");
const GenericIterator = @import("../collections/iterator.zig").Entry;
pub fn registerTypes() []const type {
return &.{
URLSearchParams,
KeyIterator,
ValueIterator,
EntryIterator,
};
}
const KeyValueList = @import("../KeyValueList.zig");
const URLSearchParams = @This();
_arena: Allocator,
_params: Entry.List,
pub const KeyIterator = GenericIterator(Iterator, "0");
pub const ValueIterator = GenericIterator(Iterator, "1");
pub const EntryIterator = GenericIterator(Iterator, null);
_params: KeyValueList,
const InitOpts = union(enum) {
query_string: []const u8,
@@ -33,7 +21,7 @@ const InitOpts = union(enum) {
};
pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams {
const arena = page.arena;
const params: Entry.List = blk: {
const params: KeyValueList = blk: {
const opts = opts_ orelse break :blk .empty;
break :blk switch (opts) {
.query_string => |str| try paramsFromString(arena, str, &page.buf),
@@ -47,81 +35,64 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams {
}
pub fn getSize(self: *const URLSearchParams) usize {
return self._params.items.len;
return self._params.len();
}
pub fn get(self: *const URLSearchParams, name: []const u8) ?[]const u8 {
const entry = self.getEntry(name) orelse return null;
return entry.value.str();
return self._params.get(name);
}
pub fn getAll(self: *const URLSearchParams, name: []const u8, page: *Page) ![]const []const u8 {
const arena = page.call_arena;
var arr: std.ArrayList([]const u8) = .empty;
for (self._params.items) |*entry| {
if (entry.name.eqlSlice(name)) {
try arr.append(arena, entry.value.str());
}
}
return arr.items;
return self._params.getAll(name, page);
}
pub fn has(self: *const URLSearchParams, name: []const u8) bool {
return self.getEntry(name) != null;
return self._params.has(name);
}
pub fn set(self: *URLSearchParams, name: []const u8, value: []const u8) !void {
self.delete(name, null);
return self.append(name, value);
return self._params.set(self._arena, name, value);
}
pub fn append(self: *URLSearchParams, name: []const u8, value: []const u8) !void {
const arena = self._arena;
return self._params.append(arena, .{
.name = try String.init(arena, name, .{}),
.value = try String.init(arena, value, .{}),
});
return self._params.append(self._arena, name, value);
}
pub fn delete(self: *URLSearchParams, name: []const u8, value: ?[]const u8) void {
var i: usize = 0;
while (i < self._params.items.len) {
const entry = self._params.items[i];
if (entry.name.eqlSlice(name)) {
if (value == null or entry.value.eqlSlice(value.?)) {
_ = self._params.swapRemove(i);
continue;
}
}
i += 1;
}
self._params.delete(name, value);
}
pub fn keys(self: *const URLSearchParams, page: *Page) !*KeyIterator {
return .init(.{ .list = self }, page);
pub fn keys(self: *URLSearchParams, page: *Page) !*KeyValueList.KeyIterator {
return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._params }, page);
}
pub fn values(self: *const URLSearchParams, page: *Page) !*ValueIterator {
return .init(.{ .list = self }, page);
pub fn values(self: *URLSearchParams, page: *Page) !*KeyValueList.ValueIterator {
return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._params }, page);
}
pub fn entries(self: *const URLSearchParams, page: *Page) !*EntryIterator {
return .init(.{ .list = self }, page);
pub fn entries(self: *URLSearchParams, page: *Page) !*KeyValueList.EntryIterator {
return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._params }, page);
}
pub fn toString(self: *const URLSearchParams, writer: *std.Io.Writer) !void {
const items = self._params.items;
const items = self._params._entries.items;
if (items.len == 0) {
return;
}
try items[0].toString(writer);
try writeEntry(&items[0], writer);
for (items[1..]) |entry| {
try writer.writeByte('&');
try entry.toString(writer);
try writeEntry(&entry, writer);
}
}
fn writeEntry(entry: *const KeyValueList.Entry, writer: *std.Io.Writer) !void {
try escape(entry.name.str(), writer);
try writer.writeByte('=');
try escape(entry.value.str(), writer);
}
pub fn format(self: *const URLSearchParams, writer: *std.Io.Writer) !void {
return self.toString(writer);
}
@@ -129,7 +100,7 @@ pub fn format(self: *const URLSearchParams, writer: *std.Io.Writer) !void {
pub fn forEach(self: *URLSearchParams, cb_: js.Function, js_this_: ?js.Object) !void {
const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;
for (self._params.items) |entry| {
for (self._params._entries.items) |entry| {
cb.call(void, .{ entry.value.str(), entry.name.str(), self }) catch |err| {
// this is a non-JS error
log.warn(.js, "URLSearchParams.forEach", .{ .err = err });
@@ -138,23 +109,14 @@ pub fn forEach(self: *URLSearchParams, cb_: js.Function, js_this_: ?js.Object) !
}
pub fn sort(self: *URLSearchParams) void {
std.mem.sort(Entry, self._params.items, {}, entryLessThan);
}
fn entryLessThan(_: void, a: Entry, b: Entry) bool {
return std.mem.order(u8, a.name.str(), b.name.str()) == .lt;
}
fn getEntry(self: *const URLSearchParams, name: []const u8) ?*Entry {
for (self._params.items) |*entry| {
if (entry.name.eqlSlice(name)) {
return entry;
std.mem.sort(KeyValueList.Entry, self._params._entries.items, {}, struct {
fn cmp(_: void, a: KeyValueList.Entry, b: KeyValueList.Entry) bool {
return std.mem.order(u8, a.name.str(), b.name.str()) == .lt;
}
}
return null;
}.cmp);
}
fn paramsFromString(arena: Allocator, input_: []const u8, buf: []u8) !Entry.List {
fn paramsFromString(allocator: Allocator, input_: []const u8, buf: []u8) !KeyValueList {
if (input_.len == 0) {
return .empty;
}
@@ -169,21 +131,24 @@ fn paramsFromString(arena: Allocator, input_: []const u8, buf: []u8) !Entry.List
return .empty;
}
var params: Entry.List = .empty;
var params = KeyValueList.init();
var it = std.mem.splitScalar(u8, input, '&');
while (it.next()) |entry| {
var name: String = undefined;
var value: String = undefined;
if (std.mem.indexOfScalarPos(u8, entry, 0, '=')) |idx| {
name = try unescape(arena, entry[0..idx], buf);
value = try unescape(arena, entry[idx + 1 ..], buf);
name = try unescape(allocator, entry[0..idx], buf);
value = try unescape(allocator, entry[idx + 1 ..], buf);
} else {
name = try unescape(arena, entry, buf);
name = try unescape(allocator, entry, buf);
value = String.init(undefined, "", .{}) catch unreachable;
}
try params.append(arena, .{
// optimized, unescape returns a String directly (Because unescape may
// have to dupe itself, so it knows how best to create the String)
try params._entries.append(allocator, .{
.name = name,
.value = value,
});
@@ -192,19 +157,6 @@ fn paramsFromString(arena: Allocator, input_: []const u8, buf: []u8) !Entry.List
return params;
}
const Entry = struct {
name: String,
value: String,
const List = std.ArrayListUnmanaged(Entry);
pub fn toString(self: *const Entry, writer: *std.Io.Writer) !void {
try escape(self.name.str(), writer);
try writer.writeByte('=');
try escape(self.value.str(), writer);
}
};
fn unescape(arena: Allocator, value: []const u8, buf: []u8) !String {
if (value.len == 0) {
return String.init(undefined, "", .{});