diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index b74331f4..4f294f17 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -18,6 +18,7 @@ const std = @import("std"); const log = @import("../../log.zig"); +const string = @import("../../string.zig"); const js = @import("js.zig"); const bridge = @import("bridge.zig"); @@ -619,6 +620,19 @@ fn jsValueToStruct(self: *const Local, comptime T: type, js_val: js.Value) !?T { }; return try promise.persist(); }, + string.String => { + if (!js_val.isString()) { + return null; + } + return try self.valueToStringSSO(js_val, .{.allocator = self.ctx.call_arena}); + }, + string.Global => { + if (!js_val.isString()) { + return null; + } + // Use arena for persistent strings + return .{.str = try self.valueToStringSSO(js_val, .{ .allocator = self.ctx.arena }) }; + }, else => { if (!js_val.isObject()) { return null; @@ -927,6 +941,15 @@ fn probeJsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !Pr } }, .@"struct" => { + // Handle string.String and string.Global specially + if (T == string.String or T == string.Global) { + if (js_val.isString()) { + return .{ .ok = {} }; + } + // Anything can be coerced to a string + return .{ .coerce = {} }; + } + // We don't want to duplicate the code for this, so we call // the actual conversion function. const value = (try self.jsValueToStruct(T, js_val)) orelse { @@ -1118,6 +1141,46 @@ fn _jsStringToZig(self: *const Local, comptime null_terminate: bool, str: anytyp return buf; } +// Convert JS string to string.String with SSO +pub fn valueToStringSSO(self: *const Local, js_val: js.Value, opts: ToStringOpts) !string.String { + const string_handle = v8.v8__Value__ToString(js_val.handle, self.handle) orelse { + return error.JsException; + }; + return self.jsStringToStringSSO(string_handle, opts); +} + +pub fn jsStringToStringSSO(self: *const Local, str: anytype, opts: ToStringOpts) !string.String { + const handle = if (@TypeOf(str) == js.String) str.handle else str; + const len: usize = @intCast(v8.v8__String__Utf8Length(handle, self.isolate.handle)); + + if (len <= 12) { + var content: [12]u8 = @splat(0); + const n = v8.v8__String__WriteUtf8(handle, self.isolate.handle, &content, len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8); + if (comptime IS_DEBUG) { + std.debug.assert(n == len); + } + return .{ .len = @intCast(len), .payload = .{ .content = content } }; + } + + const allocator = opts.allocator orelse self.call_arena; + const buf = try allocator.alloc(u8, len); + const n = v8.v8__String__WriteUtf8(handle, self.isolate.handle, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8); + if (comptime IS_DEBUG) { + std.debug.assert(n == len); + } + + var prefix: [4]u8 = @splat(0); + @memcpy(&prefix, buf[0..4]); + + return .{ + .len = @intCast(len), + .payload = .{ .heap = .{ + .prefix = prefix, + .ptr = buf.ptr, + } }, + }; +} + // == Promise Helpers == pub fn rejectPromise(self: *const Local, value: anytype) !js.Promise { var resolver = js.PromiseResolver.init(self); diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 34a96976..fe61346f 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -282,12 +282,12 @@ pub fn getSelection(self: *Document) *Selection { return &self._selection; } -pub fn querySelector(self: *Document, input: []const u8, page: *Page) !?*Element { - return Selector.querySelector(self.asNode(), input, page); +pub fn querySelector(self: *Document, input: String, page: *Page) !?*Element { + return Selector.querySelector(self.asNode(), input.str(), page); } -pub fn querySelectorAll(self: *Document, input: []const u8, page: *Page) !*Selector.List { - return Selector.querySelectorAll(self.asNode(), input, page); +pub fn querySelectorAll(self: *Document, input: String, page: *Page) !*Selector.List { + return Selector.querySelectorAll(self.asNode(), input.str(), page); } pub fn getImplementation(_: *const Document) DOMImplementation { diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index b4d6c42a..c2ce4e3f 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -344,7 +344,7 @@ test "cdp Node: Registry register" { var doc = page.window._document; { - const dom_node = (try doc.querySelector("#a1", page)).?.asNode(); + const dom_node = (try doc.querySelector(testing.newString("#a1"), page)).?.asNode(); const node = try registry.register(dom_node); const n1b = registry.lookup_by_id.get(1).?; const n1c = registry.lookup_by_node.get(node.dom).?; @@ -356,7 +356,7 @@ test "cdp Node: Registry register" { } { - const dom_node = (try doc.querySelector("p", page)).?.asNode(); + const dom_node = (try doc.querySelector(testing.newString("p"), page)).?.asNode(); const node = try registry.register(dom_node); const n1b = registry.lookup_by_id.get(2).?; const n1c = registry.lookup_by_node.get(node.dom).?; @@ -400,18 +400,18 @@ test "cdp Node: search list" { defer page._session.removePage(); var doc = page.window._document; - const s1 = try search_list.create((try doc.querySelectorAll("a", page))._nodes); + const s1 = try search_list.create((try doc.querySelectorAll(testing.newString("a"), page))._nodes); try testing.expectEqual("1", s1.name); try testing.expectEqualSlices(u32, &.{ 1, 2 }, s1.node_ids); try testing.expectEqual(2, registry.lookup_by_id.count()); try testing.expectEqual(2, registry.lookup_by_node.count()); - const s2 = try search_list.create((try doc.querySelectorAll("#a1", page))._nodes); + const s2 = try search_list.create((try doc.querySelectorAll(testing.newString("#a1"), page))._nodes); try testing.expectEqual("2", s2.name); try testing.expectEqualSlices(u32, &.{1}, s2.node_ids); - const s3 = try search_list.create((try doc.querySelectorAll("#a2", page))._nodes); + const s3 = try search_list.create((try doc.querySelectorAll(testing.newString("#a2"), page))._nodes); try testing.expectEqual("3", s3.name); try testing.expectEqualSlices(u32, &.{2}, s3.node_ids); diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 52042849..4de1e59f 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -33,6 +33,7 @@ pub const expectEqual = base.expectEqual; pub const expectError = base.expectError; pub const expectEqualSlices = base.expectEqualSlices; pub const pageTest = base.pageTest; +pub const newString = base.newString; const Client = struct { allocator: Allocator, diff --git a/src/http/Http.zig b/src/http/Http.zig index 2465eaf1..f54fd6e4 100644 --- a/src/http/Http.zig +++ b/src/http/Http.zig @@ -271,17 +271,21 @@ pub const Header = struct { }; pub const Headers = struct { - headers: *c.curl_slist, + headers: ?*c.curl_slist, cookies: ?[*c]const u8, pub fn init(user_agent: [:0]const u8) !Headers { const header_list = c.curl_slist_append(null, user_agent); - if (header_list == null) return error.OutOfMemory; + if (header_list == null) { + return error.OutOfMemory; + } return .{ .headers = header_list, .cookies = null }; } pub fn deinit(self: *const Headers) void { - c.curl_slist_free_all(self.headers); + if (self.headers) |hdr| { + c.curl_slist_free_all(hdr); + } } pub fn add(self: *Headers, header: [*c]const u8) !void { diff --git a/src/string.zig b/src/string.zig index 90966d88..f2f5c3c5 100644 --- a/src/string.zig +++ b/src/string.zig @@ -188,6 +188,13 @@ pub const String = packed struct { } }; +// Discriminatory type that signals the bridge to use arena instead of call_arena +// Use this for strings that need to persist beyond the current call +// The caller can unwrap and store just the underlying .str field +pub const Global = struct { + str: String, +}; + fn asUint(comptime string: anytype) std.meta.Int( .unsigned, @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0 diff --git a/src/testing.zig b/src/testing.zig index c022d830..3a71a0ef 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -176,6 +176,11 @@ pub fn print(comptime fmt: []const u8, args: anytype) void { } } +const String = @import("string.zig").String; +pub fn newString(str: []const u8) String { + return String.init(arena_allocator, str, .{}) catch unreachable; +} + pub const Random = struct { var instance: ?std.Random.DefaultPrng = null;