From c7580542502c05138a5add6891f4bef9cb96c115 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 5 Jun 2025 09:44:36 +0800 Subject: [PATCH] URLSearchParam constructor support for object initialization This adds support for: ``` new URLSearchParams({over: 9000}); ``` The spec says that any thing that produces/iterates a sequence of string pairs is valid. By using the lower-level JsObject, this hopefully takes care of the most common cases. But I don't think it's complete, and I don't think we currently capture enough data to make this work. There's no way for the JS runtime to know if a value (say, a netsurf instance, or even a Zig instance) provides an string=>string iterator. --- src/browser/key_value.zig | 7 +++++++ src/browser/url/url.zig | 24 +++++++++++++++++++++++ src/runtime/js.zig | 40 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/src/browser/key_value.zig b/src/browser/key_value.zig index a4000220..5875d4e1 100644 --- a/src/browser/key_value.zig +++ b/src/browser/key_value.zig @@ -102,6 +102,13 @@ pub const List = struct { }); } + pub fn appendOwnedAssumeCapacity(self: *List, key: []const u8, value: []const u8) void { + self.entries.appendAssumeCapacity(.{ + .key = key, + .value = value, + }); + } + pub fn delete(self: *List, key: []const u8) void { var i: usize = 0; while (i < self.entries.items.len) { diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig index 3045670a..9ec1e04b 100644 --- a/src/browser/url/url.zig +++ b/src/browser/url/url.zig @@ -20,6 +20,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const parser = @import("../netsurf.zig"); +const Env = @import("../env.zig").Env; const Page = @import("../page.zig").Page; const FormData = @import("../xhr/form_data.zig").FormData; const HTMLElement = @import("../html/elements.zig").HTMLElement; @@ -247,12 +248,31 @@ pub const URLSearchParams = struct { const URLSearchParamsOpts = union(enum) { qs: []const u8, form_data: *const FormData, + js_obj: Env.JsObject, }; pub fn constructor(opts_: ?URLSearchParamsOpts, page: *Page) !URLSearchParams { const opts = opts_ orelse return .{ .entries = .{} }; return switch (opts) { .qs => |qs| init(page.arena, qs), .form_data => |fd| .{ .entries = try fd.entries.clone(page.arena) }, + .js_obj => |js_obj| { + const arena = page.arena; + var it = js_obj.nameIterator(); + + var entries: kv.List = .{}; + try entries.ensureTotalCapacity(arena, it.count); + + while (try it.next()) |js_name| { + const name = try js_name.toString(arena); + const js_val = try js_obj.get(name); + entries.appendOwnedAssumeCapacity( + name, + try js_val.toString(arena), + ); + } + + return .{ .entries = entries }; + }, }; } @@ -613,5 +633,9 @@ test "Browser.URLSearchParams" { .{ "ups.getAll('b')", "3" }, .{ "fd.delete('a')", null }, // the two aren't linked, it created a copy .{ "ups.size", "3" }, + .{ "ups = new URLSearchParams({over: 9000, spice: 'flow'})", null }, + .{ "ups.size", "2" }, + .{ "ups.getAll('over')", "9000" }, + .{ "ups.getAll('spice')", "flow" }, }, .{}); } diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 4bde54db..800ab03e 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -1366,6 +1366,13 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { } } + pub fn get(self: JsObject, key: []const u8) !Value { + const scope = self.scope; + const js_key = v8.String.initUtf8(scope.isolate, key); + const js_val = try self.js_obj.getValue(scope.context, js_key); + return scope.createValue(js_val); + } + pub fn isTruthy(self: JsObject) bool { const js_value = self.js_obj.toValue(); return js_value.toBool(self.scope.isolate); @@ -1421,6 +1428,20 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { pub fn isNullOrUndefined(self: JsObject) bool { return self.js_obj.toValue().isNullOrUndefined(); } + + pub fn nameIterator(self: JsObject) ValueIterator { + const scope = self.scope; + const js_obj = self.js_obj; + + const array = js_obj.getPropertyNames(scope.context); + const count = array.length(); + + return .{ + .count = count, + .scope = scope, + .js_obj = array.castTo(v8.Object), + }; + } }; // This only exists so that we know whether a function wants the opaque @@ -1626,6 +1647,25 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { } }; + pub const ValueIterator = struct { + count: u32, + idx: u32 = 0, + js_obj: v8.Object, + scope: *const Scope, + + pub fn next(self: *ValueIterator) !?Value { + const idx = self.idx; + if (idx == self.count) { + return null; + } + self.idx += 1; + + const scope = self.scope; + const js_val = try self.js_obj.getAtIndex(scope.context, idx); + return scope.createValue(js_val); + } + }; + fn compileModule(isolate: v8.Isolate, src: []const u8, name: []const u8) !v8.Module { // compile const script_name = v8.String.initUtf8(isolate, name);