diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 1186d54f..01a67e72 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -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"), diff --git a/src/browser/tests/polyfill/webcomponents.html b/src/browser/tests/polyfill/webcomponents.html new file mode 100644 index 00000000..5854bc82 --- /dev/null +++ b/src/browser/tests/polyfill/webcomponents.html @@ -0,0 +1,23 @@ + + + +
+ + diff --git a/src/browser/webapi/KeyValueList.zig b/src/browser/webapi/KeyValueList.zig new file mode 100644 index 00000000..9f0cca7c --- /dev/null +++ b/src/browser/webapi/KeyValueList.zig @@ -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); diff --git a/src/browser/webapi/net/FormData.zig b/src/browser/webapi/net/FormData.zig new file mode 100644 index 00000000..969cb0b1 --- /dev/null +++ b/src/browser/webapi/net/FormData.zig @@ -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", .{}); +} diff --git a/src/browser/webapi/net/URLSearchParams.zig b/src/browser/webapi/net/URLSearchParams.zig index b1f8d9fe..70d5cbeb 100644 --- a/src/browser/webapi/net/URLSearchParams.zig +++ b/src/browser/webapi/net/URLSearchParams.zig @@ -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, "", .{});