// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire // // 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 . const std = @import("std"); const jsruntime = @import("jsruntime"); const Case = jsruntime.test_utils.Case; const checkCases = jsruntime.test_utils.checkCases; const generate = @import("../generate.zig"); const query = @import("query.zig"); pub const Interfaces = generate.Tuple(.{ URL, URLSearchParams, }); // https://url.spec.whatwg.org/#url // // TODO we could avoid many of these getter string allocation in two differents // way: // // 1. We can eventually get the slice of scheme *with* the following char in // the underlying string. But I don't know if it's possible and how to do that. // I mean, if the rawuri contains `https://foo.bar`, uri.scheme is a slice // containing only `https`. I want `https:` so, in theory, I don't need to // allocate data, I should be able to retrieve the scheme + the following `:` // from rawuri. // // 2. The other way would bu to copy the `std.Uri` code to ahve a dedicated // parser including the characters we want for the web API. pub const URL = struct { rawuri: []const u8, uri: std.Uri, search_params: URLSearchParams, pub const mem_guarantied = true; pub fn constructor(alloc: std.mem.Allocator, url: []const u8, base: ?[]const u8) !URL { const raw = try std.mem.concat(alloc, u8, &[_][]const u8{ url, base orelse "" }); errdefer alloc.free(raw); const uri = std.Uri.parse(raw) catch { return error.TypeError; }; return .{ .rawuri = raw, .uri = uri, .search_params = try URLSearchParams.constructor( alloc, uriComponentNullStr(uri.query), ), }; } pub fn deinit(self: *URL, alloc: std.mem.Allocator) void { self.search_params.deinit(alloc); alloc.free(self.rawuri); } // the caller must free the returned string. // TODO return a disposable string // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_origin(self: *URL, alloc: std.mem.Allocator) ![]const u8 { var buf = std.ArrayList(u8).init(alloc); defer buf.deinit(); try self.uri.writeToStream(.{ .scheme = true, .authentication = false, .authority = true, .path = false, .query = false, .fragment = false, }, buf.writer()); return try buf.toOwnedSlice(); } // get_href returns the URL by writing all its components. // The query is replaced by a dump of search params. // // the caller must free the returned string. // TODO return a disposable string // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_href(self: *URL, alloc: std.mem.Allocator) ![]const u8 { // retrieve the query search from search_params. const cur = self.uri.query; defer self.uri.query = cur; var q = std.ArrayList(u8).init(alloc); defer q.deinit(); try self.search_params.values.encode(q.writer()); self.uri.query = .{ .percent_encoded = q.items }; return try self.format(alloc); } // format the url with all its components. pub fn format(self: *URL, alloc: std.mem.Allocator) ![]const u8 { var buf = std.ArrayList(u8).init(alloc); defer buf.deinit(); try self.uri.writeToStream(.{ .scheme = true, .authentication = true, .authority = true, .path = uriComponentNullStr(self.uri.path).len > 0, .query = uriComponentNullStr(self.uri.query).len > 0, .fragment = uriComponentNullStr(self.uri.fragment).len > 0, }, buf.writer()); return try buf.toOwnedSlice(); } // the caller must free the returned string. // TODO return a disposable string // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_protocol(self: *URL, alloc: std.mem.Allocator) ![]const u8 { return try std.mem.concat(alloc, u8, &[_][]const u8{ self.uri.scheme, ":" }); } pub fn get_username(self: *URL) []const u8 { return uriComponentNullStr(self.uri.user); } pub fn get_password(self: *URL) []const u8 { return uriComponentNullStr(self.uri.password); } // the caller must free the returned string. // TODO return a disposable string // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_host(self: *URL, alloc: std.mem.Allocator) ![]const u8 { var buf = std.ArrayList(u8).init(alloc); defer buf.deinit(); try self.uri.writeToStream(.{ .scheme = false, .authentication = false, .authority = true, .path = false, .query = false, .fragment = false, }, buf.writer()); return try buf.toOwnedSlice(); } pub fn get_hostname(self: *URL) []const u8 { return uriComponentNullStr(self.uri.host); } // the caller must free the returned string. // TODO return a disposable string // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_port(self: *URL, alloc: std.mem.Allocator) ![]const u8 { if (self.uri.port == null) return try alloc.dupe(u8, ""); var buf = std.ArrayList(u8).init(alloc); defer buf.deinit(); try std.fmt.formatInt(self.uri.port.?, 10, .lower, .{}, buf.writer()); return try buf.toOwnedSlice(); } pub fn get_pathname(self: *URL) []const u8 { if (uriComponentStr(self.uri.path).len == 0) return "/"; return uriComponentStr(self.uri.path); } // the caller must free the returned string. // TODO return a disposable string // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_search(self: *URL, alloc: std.mem.Allocator) ![]const u8 { if (self.search_params.get_size() == 0) return try alloc.dupe(u8, ""); var buf: std.ArrayListUnmanaged(u8) = .{}; defer buf.deinit(alloc); try buf.append(alloc, '?'); try self.search_params.values.encode(buf.writer(alloc)); return buf.toOwnedSlice(alloc); } // the caller must free the returned string. // TODO return a disposable string // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_hash(self: *URL, alloc: std.mem.Allocator) ![]const u8 { if (self.uri.fragment == null) return try alloc.dupe(u8, ""); return try std.mem.concat(alloc, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) }); } pub fn get_searchParams(self: *URL) *URLSearchParams { return &self.search_params; } pub fn _toJSON(self: *URL, alloc: std.mem.Allocator) ![]const u8 { return try self.get_href(alloc); } }; // uriComponentNullStr converts an optional std.Uri.Component to string value. // The string value can be undecoded. fn uriComponentNullStr(c: ?std.Uri.Component) []const u8 { if (c == null) return ""; return uriComponentStr(c.?); } fn uriComponentStr(c: std.Uri.Component) []const u8 { return switch (c) { .raw => |v| v, .percent_encoded => |v| v, }; } // https://url.spec.whatwg.org/#interface-urlsearchparams // TODO array like pub const URLSearchParams = struct { values: query.Values, pub const mem_guarantied = true; pub fn constructor(alloc: std.mem.Allocator, init: ?[]const u8) !URLSearchParams { return .{ .values = try query.parseQuery(alloc, init orelse ""), }; } pub fn deinit(self: *URLSearchParams, _: std.mem.Allocator) void { self.values.deinit(); } pub fn get_size(self: *URLSearchParams) u32 { return @intCast(self.values.count()); } pub fn _append(self: *URLSearchParams, name: []const u8, value: []const u8) !void { try self.values.append(name, value); } pub fn _delete(self: *URLSearchParams, name: []const u8, value: ?[]const u8) !void { if (value) |v| return self.values.deleteValue(name, v); self.values.delete(name); } pub fn _get(self: *URLSearchParams, name: []const u8) ?[]const u8 { return self.values.first(name); } // TODO return generates an error: caught unexpected error 'TypeLookup' // pub fn _getAll(self: *URLSearchParams, name: []const u8) [][]const u8 { // try self.values.get(name); // } // TODO pub fn _sort(_: *URLSearchParams) void {} }; // Tests // ----- pub fn testExecFn( _: std.mem.Allocator, js_env: *jsruntime.Env, ) anyerror!void { var url = [_]Case{ .{ .src = "var url = new URL('https://foo.bar/path?query#fragment')", .ex = "undefined" }, .{ .src = "url.origin", .ex = "https://foo.bar" }, .{ .src = "url.href", .ex = "https://foo.bar/path?query#fragment" }, .{ .src = "url.protocol", .ex = "https:" }, .{ .src = "url.username", .ex = "" }, .{ .src = "url.password", .ex = "" }, .{ .src = "url.host", .ex = "foo.bar" }, .{ .src = "url.hostname", .ex = "foo.bar" }, .{ .src = "url.port", .ex = "" }, .{ .src = "url.pathname", .ex = "/path" }, .{ .src = "url.search", .ex = "?query" }, .{ .src = "url.hash", .ex = "#fragment" }, .{ .src = "url.searchParams.get('query')", .ex = "" }, }; try checkCases(js_env, &url); var qs = [_]Case{ .{ .src = "var url = new URL('https://foo.bar/path?a=~&b=%7E#fragment')", .ex = "undefined" }, .{ .src = "url.searchParams.get('a')", .ex = "~" }, .{ .src = "url.searchParams.get('b')", .ex = "~" }, .{ .src = "url.searchParams.append('c', 'foo')", .ex = "undefined" }, .{ .src = "url.searchParams.get('c')", .ex = "foo" }, .{ .src = "url.searchParams.size", .ex = "3" }, // search is dynamic .{ .src = "url.search", .ex = "?a=%7E&b=%7E&c=foo" }, // href is dynamic .{ .src = "url.href", .ex = "https://foo.bar/path?a=%7E&b=%7E&c=foo#fragment" }, .{ .src = "url.searchParams.delete('c', 'foo')", .ex = "undefined" }, .{ .src = "url.searchParams.get('c')", .ex = "" }, .{ .src = "url.searchParams.delete('a')", .ex = "undefined" }, .{ .src = "url.searchParams.get('a')", .ex = "" }, }; try checkCases(js_env, &qs); }