diff --git a/build.zig b/build.zig
index e41d6553a..22ebb542b 100644
--- a/build.zig
+++ b/build.zig
@@ -384,6 +384,7 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
try buildMbedtls(b, mod);
try buildNghttp2(b, mod);
try buildCurl(b, mod);
+ try buildAda(b, mod);
switch (target.result.os.tag) {
.macos => {
@@ -849,3 +850,34 @@ fn buildCurl(b: *Build, m: *Build.Module) !void {
},
});
}
+
+pub fn buildAda(b: *Build, m: *Build.Module) !void {
+ const ada_dep = b.dependency("ada-singleheader", .{});
+
+ const ada_mod = b.createModule(.{
+ .root_source_file = b.path("vendor/ada/root.zig"),
+ });
+
+ const ada_lib = b.addLibrary(.{
+ .name = "ada",
+ .root_module = b.createModule(.{
+ .link_libcpp = true,
+ .target = m.resolved_target,
+ .optimize = m.optimize,
+ }),
+ .linkage = .static,
+ });
+
+ ada_lib.addCSourceFile(.{
+ .file = ada_dep.path("ada.cpp"),
+ .flags = &.{ "-std=c++20", "-O3" },
+ .language = .cpp,
+ });
+
+ ada_lib.installHeader(ada_dep.path("ada_c.h"), "ada_c.h");
+
+ // Link the library to ada module.
+ ada_mod.linkLibrary(ada_lib);
+ // Expose ada module to main module.
+ m.addImport("ada", ada_mod);
+}
diff --git a/build.zig.zon b/build.zig.zon
index 9d57095f9..6e4f544b9 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -9,5 +9,9 @@
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
},
//.v8 = .{ .path = "../zig-v8-fork" }
+ .@"ada-singleheader" = .{
+ .url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
+ .hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
+ },
},
}
diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig
index 4020b4980..0516e76d1 100644
--- a/src/browser/html/document.zig
+++ b/src/browser/html/document.zig
@@ -42,12 +42,12 @@ pub const HTMLDocument = struct {
// JS funcs
// --------
- pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
+ pub fn get_domain(self: *parser.DocumentHTML) ![]const u8 {
// libdom's document_html get_domain always returns null, this is
// the way MDN recommends getting the domain anyways, since document.domain
// is deprecated.
const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
- return location.get_host(page);
+ return location.get_host();
}
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig
index 9673db692..8667c04f5 100644
--- a/src/browser/html/elements.zig
+++ b/src/browser/html/elements.zig
@@ -218,36 +218,36 @@ pub const HTMLAnchorElement = struct {
}
pub fn get_href(self: *parser.Anchor) ![]const u8 {
- return try parser.anchorGetHref(self);
+ return parser.anchorGetHref(self);
}
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void {
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
- return try parser.anchorSetHref(self, full);
+ return parser.anchorSetHref(self, full);
}
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
- return try parser.anchorGetHrefLang(self);
+ return parser.anchorGetHrefLang(self);
}
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
- return try parser.anchorSetHrefLang(self, href);
+ return parser.anchorSetHrefLang(self, href);
}
pub fn get_type(self: *parser.Anchor) ![]const u8 {
- return try parser.anchorGetType(self);
+ return parser.anchorGetType(self);
}
pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
- return try parser.anchorSetType(self, t);
+ return parser.anchorSetType(self, t);
}
pub fn get_rel(self: *parser.Anchor) ![]const u8 {
- return try parser.anchorGetRel(self);
+ return parser.anchorGetRel(self);
}
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
- return try parser.anchorSetRel(self, t);
+ return parser.anchorSetRel(self, t);
}
pub fn get_text(self: *parser.Anchor) !?[]const u8 {
@@ -269,182 +269,175 @@ pub const HTMLAnchorElement = struct {
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
}
- return .empty;
+ return error.NotProvided;
}
- // TODO return a disposable string
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
- return try u.get_origin(page);
+ defer u.destructor();
+ return u.get_origin(page);
}
- // TODO return a disposable string
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
- return u.get_protocol();
+ defer u.destructor();
+
+ return page.call_arena.dupe(u8, u.get_protocol());
}
- pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
- const arena = page.arena;
+ pub fn set_protocol(self: *parser.Anchor, protocol: []const u8, page: *Page) !void {
var u = try url(self, page);
+ defer u.destructor();
+ try u.set_protocol(protocol);
- u.uri.scheme = v;
- const href = try u.toString(arena);
- try parser.anchorSetHref(self, href);
+ const href = try u._toString(page);
+ return parser.anchorSetHref(self, href);
}
- // TODO return a disposable string
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
- var u = try url(self, page);
- return try u.get_host(page);
- }
-
- pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void {
- // search : separator
- var p: ?u16 = null;
- var h: []const u8 = undefined;
- for (v, 0..) |c, i| {
- if (c == ':') {
- h = v[0..i];
- p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
- break;
- }
- }
+ var u = url(self, page) catch return "";
+ defer u.destructor();
- const arena = page.arena;
- var u = try url(self, page);
+ return page.call_arena.dupe(u8, u.get_host());
+ }
- if (p) |pp| {
- u.uri.host = .{ .raw = h };
- u.uri.port = pp;
- } else {
- u.uri.host = .{ .raw = v };
- u.uri.port = null;
- }
+ pub fn set_host(self: *parser.Anchor, host: []const u8, page: *Page) !void {
+ var u = try url(self, page);
+ defer u.destructor();
+ try u.set_host(host);
- const href = try u.toString(arena);
- try parser.anchorSetHref(self, href);
+ const href = try u._toString(page);
+ return parser.anchorSetHref(self, href);
}
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
- var u = try url(self, page);
- return u.get_hostname();
+ var u = url(self, page) catch return "";
+ defer u.destructor();
+ return page.call_arena.dupe(u8, u.get_hostname());
}
- pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
- const arena = page.arena;
+ pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void {
var u = try url(self, page);
- u.uri.host = .{ .raw = v };
- const href = try u.toString(arena);
- try parser.anchorSetHref(self, href);
+ defer u.destructor();
+ try u.set_hostname(hostname);
+
+ const href = try u._toString(page);
+ return parser.anchorSetHref(self, href);
}
- // TODO return a disposable string
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
- var u = try url(self, page);
- return try u.get_port(page);
+ var u = url(self, page) catch return "";
+ defer u.destructor();
+ return page.call_arena.dupe(u8, u.get_port());
}
- pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
- const arena = page.arena;
+ pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
+ defer u.destructor();
- if (v != null and v.?.len > 0) {
- u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
+ if (maybe_port) |port| {
+ try u.set_port(port);
} else {
- u.uri.port = null;
+ u.clearPort();
}
- const href = try u.toString(arena);
- try parser.anchorSetHref(self, href);
+ const href = try u._toString(page);
+ return parser.anchorSetHref(self, href);
}
- // TODO return a disposable string
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
- var u = try url(self, page);
- return u.get_username();
+ var u = url(self, page) catch return "";
+ defer u.destructor();
+
+ const username = u.get_username();
+ if (username.len == 0) {
+ return "";
+ }
+
+ return page.call_arena.dupe(u8, username);
}
- pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
- const arena = page.arena;
+ pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
+ defer u.destructor();
- if (v) |vv| {
- u.uri.user = .{ .raw = vv };
- } else {
- u.uri.user = null;
- }
- const href = try u.toString(arena);
+ const username = if (maybe_username) |username| username else "";
+ try u.set_username(username);
- try parser.anchorSetHref(self, href);
+ const href = try u._toString(page);
+ return parser.anchorSetHref(self, href);
}
- // TODO return a disposable string
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
- var u = try url(self, page);
- return try page.arena.dupe(u8, u.get_password());
+ var u = url(self, page) catch return "";
+ defer u.destructor();
+
+ return page.call_arena.dupe(u8, u.get_password());
}
- pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
- const arena = page.arena;
+ pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
+ defer u.destructor();
- if (v) |vv| {
- u.uri.password = .{ .raw = vv };
- } else {
- u.uri.password = null;
- }
- const href = try u.toString(arena);
+ const password = if (maybe_password) |password| password else "";
+ try u.set_password(password);
- try parser.anchorSetHref(self, href);
+ const href = try u._toString(page);
+ return parser.anchorSetHref(self, href);
}
- // TODO return a disposable string
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
- var u = try url(self, page);
- return u.get_pathname();
+ var u = url(self, page) catch return "";
+ defer u.destructor();
+
+ return page.call_arena.dupe(u8, u.get_pathname());
}
- pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
- const arena = page.arena;
+ pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void {
var u = try url(self, page);
- u.uri.path = .{ .raw = v };
- const href = try u.toString(arena);
+ defer u.destructor();
+
+ try u.set_pathname(pathname);
- try parser.anchorSetHref(self, href);
+ const href = try u._toString(page);
+ return parser.anchorSetHref(self, href);
}
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
- var u = try url(self, page);
- return try u.get_search(page);
+ var u = url(self, page) catch return "";
+ defer u.destructor();
+ // This allocates in page arena so no need to dupe.
+ return u.get_search(page);
}
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
+ defer u.destructor();
try u.set_search(v, page);
- const href = try u.toString(page.call_arena);
- try parser.anchorSetHref(self, href);
+ const href = try u._toString(page);
+ return parser.anchorSetHref(self, href);
}
- // TODO return a disposable string
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
- var u = try url(self, page);
- return try u.get_hash(page);
+ var u = url(self, page) catch return "";
+ defer u.destructor();
+
+ return page.call_arena.dupe(u8, u.get_hash());
}
- pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
- const arena = page.arena;
+ pub fn set_hash(self: *parser.Anchor, maybe_hash: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
+ defer u.destructor();
- if (v) |vv| {
- u.uri.fragment = .{ .raw = vv };
+ if (maybe_hash) |hash| {
+ try u.set_hash(hash);
} else {
- u.uri.fragment = null;
+ u.clearHash();
}
- const href = try u.toString(arena);
- try parser.anchorSetHref(self, href);
+ const href = try u._toString(page);
+ return parser.anchorSetHref(self, href);
}
};
diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig
index 3e3e593bc..1f8d134ca 100644
--- a/src/browser/html/location.zig
+++ b/src/browser/html/location.zig
@@ -25,13 +25,14 @@ const URL = @import("../url/url.zig").URL;
pub const Location = struct {
url: URL,
+ /// Initializes the `Location` to be used in `Window`.
/// Browsers give such initial values when user not navigated yet:
/// Chrome -> chrome://new-tab-page/
/// Firefox -> about:newtab
/// Safari -> favorites://
- pub const default = Location{
- .url = .initWithoutSearchParams(Uri.parse("about:blank") catch unreachable),
- };
+ pub fn init(url: []const u8) !Location {
+ return .{ .url = try .initForLocation(url) };
+ }
pub fn get_href(self: *Location, page: *Page) ![]const u8 {
return self.url.get_href(page);
@@ -45,16 +46,16 @@ pub const Location = struct {
return self.url.get_protocol();
}
- pub fn get_host(self: *Location, page: *Page) ![]const u8 {
- return self.url.get_host(page);
+ pub fn get_host(self: *Location) []const u8 {
+ return self.url.get_host();
}
pub fn get_hostname(self: *Location) []const u8 {
return self.url.get_hostname();
}
- pub fn get_port(self: *Location, page: *Page) ![]const u8 {
- return self.url.get_port(page);
+ pub fn get_port(self: *Location) []const u8 {
+ return self.url.get_port();
}
pub fn get_pathname(self: *Location) []const u8 {
@@ -65,8 +66,8 @@ pub const Location = struct {
return self.url.get_search(page);
}
- pub fn get_hash(self: *Location, page: *Page) ![]const u8 {
- return self.url.get_hash(page);
+ pub fn get_hash(self: *Location) []const u8 {
+ return self.url.get_hash();
}
pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
@@ -86,7 +87,7 @@ pub const Location = struct {
}
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
- return try self.get_href(page);
+ return self.get_href(page);
}
};
diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig
index ef015492a..97aa381f3 100644
--- a/src/browser/html/window.zig
+++ b/src/browser/html/window.zig
@@ -53,7 +53,7 @@ pub const Window = struct {
document: *parser.DocumentHTML,
target: []const u8 = "",
- location: Location = .default,
+ location: Location,
storage_shelf: ?*storage.Shelf = null,
// counter for having unique timer ids
@@ -79,6 +79,7 @@ pub const Window = struct {
return .{
.document = html_doc,
.target = target orelse "",
+ .location = try .init("about:blank"),
.navigator = navigator orelse .{},
.performance = Performance.init(),
};
@@ -89,6 +90,10 @@ pub const Window = struct {
try parser.documentHTMLSetLocation(Location, self.document, &self.location);
}
+ pub fn changeLocation(self: *Window, new_url: []const u8, page: *Page) !void {
+ return self.location.url.reinit(new_url, page);
+ }
+
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
self.document = doc;
diff --git a/src/browser/page.zig b/src/browser/page.zig
index 833c683f1..897cb414a 100644
--- a/src/browser/page.zig
+++ b/src/browser/page.zig
@@ -859,7 +859,7 @@ pub const Page = struct {
self.window.setStorageShelf(
try self.session.storage_shed.getOrPut(try self.origin(self.arena)),
);
- try self.window.replaceLocation(.{ .url = try self.url.toWebApi(self.arena) });
+ try self.window.changeLocation(self.url.raw, self);
}
pub const MouseEvent = struct {
diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig
index 08c97bf61..9229a23df 100644
--- a/src/browser/url/url.zig
+++ b/src/browser/url/url.zig
@@ -18,6 +18,8 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
+const Writer = std.Io.Writer;
+const ada = @import("ada");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig");
@@ -35,182 +37,223 @@ pub const Interfaces = .{
EntryIterable,
};
-// https://url.spec.whatwg.org/#url
-//
-// TODO we could avoid many of these getter string allocatoration 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
-// allocatorate data, I should be able to retrieve the scheme + the following `:`
-// from rawuri.
-//
-// 2. The other way would be to copy the `std.Uri` code to have a dedicated
-// parser including the characters we want for the web API.
+/// https://developer.mozilla.org/en-US/docs/Web/API/URL/URL
pub const URL = struct {
- uri: std.Uri,
+ internal: ada.URL,
+ /// We prefer in-house search params solution here;
+ /// ada's search params impl use more memory.
+ /// It also offers it's own iterator implementation
+ /// where we'd like to use ours.
search_params: URLSearchParams,
pub const empty = URL{
- .uri = .{ .scheme = "" },
+ .internal = null,
.search_params = .{},
};
- const URLArg = union(enum) {
- url: *URL,
- element: *parser.ElementHTML,
+ // You can use an existing URL object for either argument, and it will be
+ // stringified from the object's href property.
+ const ConstructorArg = union(enum) {
string: []const u8,
-
- fn toString(self: URLArg, arena: Allocator) !?[]const u8 {
- switch (self) {
- .string => |s| return s,
- .url => |url| return try url.toString(arena),
- .element => |e| return try parser.elementGetAttribute(@ptrCast(e), "href"),
- }
+ url: *const URL,
+ element: *parser.Element,
+
+ fn toString(self: ConstructorArg, page: *Page) ![]const u8 {
+ return switch (self) {
+ .string => |s| s,
+ .url => |url| url._toString(page),
+ .element => |e| {
+ const attrib = try parser.elementGetAttribute(@ptrCast(e), "href") orelse {
+ return error.InvalidArgument;
+ };
+
+ return attrib;
+ },
+ };
}
};
- pub fn constructor(url: URLArg, base: ?URLArg, page: *Page) !URL {
- const arena = page.arena;
- const url_str = try url.toString(arena) orelse return error.InvalidArgument;
+ pub fn constructor(url: ConstructorArg, maybe_base: ?ConstructorArg, page: *Page) !URL {
+ const url_str = try url.toString(page);
- var raw: ?[]const u8 = null;
- if (base) |b| {
- if (try b.toString(arena)) |bb| {
- raw = try @import("../../url.zig").URL.stitch(arena, url_str, bb, .{});
+ const internal = try blk: {
+ if (maybe_base) |base| {
+ break :blk ada.parseWithBase(url_str, try base.toString(page));
}
- }
- if (raw == null) {
- // if it was a URL, then it's already be owned by the arena
- raw = if (url == .url) url_str else try arena.dupe(u8, url_str);
- }
+ break :blk ada.parse(url_str);
+ };
- const uri = std.Uri.parse(raw.?) catch blk: {
- if (!std.mem.endsWith(u8, raw.?, "://")) {
- return error.TypeError;
- }
- // schema only is valid!
- break :blk std.Uri{
- .scheme = raw.?[0 .. raw.?.len - 3],
- .host = .{ .percent_encoded = "" },
- };
+ return .{
+ .internal = internal,
+ .search_params = try prepareSearchParams(page.arena, internal),
};
+ }
- return init(arena, uri);
+ pub fn destructor(self: *const URL) void {
+ // Not tracked by arena.
+ return ada.free(self.internal);
}
- pub fn init(arena: Allocator, uri: std.Uri) !URL {
- return .{
- .uri = uri,
- .search_params = try URLSearchParams.init(
- arena,
- uriComponentNullStr(uri.query),
- ),
- };
+ /// Only to be used by `Location` API. `url` MUST NOT provide search params.
+ pub fn initForLocation(url: []const u8) !URL {
+ return .{ .internal = try ada.parse(url), .search_params = .{} };
}
- pub fn initWithoutSearchParams(uri: std.Uri) URL {
- return .{ .uri = uri, .search_params = .{} };
+ /// Reinitializes the URL by parsing given `url`. Search params can be provided.
+ pub fn reinit(self: *URL, url: []const u8, page: *Page) !void {
+ _ = ada.setHref(self.internal, url);
+ if (!ada.isValid(self.internal)) return error.Internal;
+
+ self.search_params = try prepareSearchParams(page.arena, self.internal);
}
- pub fn get_origin(self: *URL, page: *Page) ![]const u8 {
- var aw = std.Io.Writer.Allocating.init(page.arena);
- try self.uri.writeToStream(&aw.writer, .{
- .scheme = true,
- .authentication = false,
- .authority = true,
- .path = false,
- .query = false,
- .fragment = false,
- });
- return aw.written();
+ /// Prepares a `URLSearchParams` from given `internal`.
+ /// Resets `search` of `internal`.
+ fn prepareSearchParams(arena: Allocator, internal: ada.URL) !URLSearchParams {
+ const maybe_search = ada.getSearchNullable(internal);
+ // Empty.
+ if (maybe_search.data == null) return .{};
+
+ const search = maybe_search.data[0..maybe_search.length];
+ const search_params = URLSearchParams.initFromString(arena, search);
+ // After a call to this function, search params are tracked by
+ // `search_params`. So we reset the internal's search.
+ ada.clearSearch(internal);
+
+ return search_params;
}
- // get_href returns the URL by writing all its components.
- pub fn get_href(self: *URL, page: *Page) ![]const u8 {
- return self.toString(page.arena);
+ pub fn clearPort(self: *const URL) void {
+ return ada.clearPort(self.internal);
}
- pub fn _toString(self: *URL, page: *Page) ![]const u8 {
- return self.toString(page.arena);
+ pub fn clearHash(self: *const URL) void {
+ return ada.clearHash(self.internal);
}
- // format the url with all its components.
- pub fn toString(self: *const URL, arena: Allocator) ![]const u8 {
- var aw = std.Io.Writer.Allocating.init(arena);
- try self.uri.writeToStream(&aw.writer, .{
- .scheme = true,
- .authentication = true,
- .authority = true,
- .path = uriComponentNullStr(self.uri.path).len > 0,
- });
+ /// Alias to get_href.
+ pub fn _toString(self: *const URL, page: *Page) ![]const u8 {
+ return self.get_href(page);
+ }
- if (self.search_params.get_size() > 0) {
- try aw.writer.writeByte('?');
- try self.search_params.write(&aw.writer);
+ // Getters.
+
+ pub fn get_searchParams(self: *URL) *URLSearchParams {
+ return &self.search_params;
+ }
+
+ pub fn get_origin(self: *const URL, page: *Page) ![]const u8 {
+ // `ada.getOriginNullable` allocates memory in order to find the `origin`.
+ // We'd like to use our arena allocator for such case;
+ // so here we allocate the `origin` in page arena and free the original.
+ const maybe_origin = ada.getOriginNullable(self.internal);
+ if (maybe_origin.data == null) {
+ return "";
}
+ defer ada.freeOwnedString(maybe_origin);
- {
- const fragment = uriComponentNullStr(self.uri.fragment);
- if (fragment.len > 0) {
- try aw.writer.writeByte('#');
- try aw.writer.writeAll(fragment);
- }
+ const origin = maybe_origin.data[0..maybe_origin.length];
+ return page.call_arena.dupe(u8, origin);
+ }
+
+ pub fn get_href(self: *const URL, page: *Page) ![]const u8 {
+ var w: Writer.Allocating = .init(page.arena);
+
+ // If URL is not valid, return immediately.
+ if (!ada.isValid(self.internal)) {
+ return "";
}
- return aw.written();
+ // Since the earlier check passed, this can't be null.
+ const str = ada.getHrefNullable(self.internal);
+ const href = str.data[0..str.length];
+ // This can't be null either.
+ const comps = ada.getComponents(self.internal);
+ // If hash provided, we write it after we fit-in the search params.
+ const has_hash = comps.hash_start != ada.URLOmitted;
+ const href_part = if (has_hash) href[0..comps.hash_start] else href;
+ try w.writer.writeAll(href_part);
+
+ // Write search params if provided.
+ if (self.search_params.get_size() > 0) {
+ try w.writer.writeByte('?');
+ try self.search_params.write(&w.writer);
+ }
+
+ // Write hash if provided before.
+ const hash = self.get_hash();
+ try w.writer.writeAll(hash);
+
+ return w.written();
}
- pub fn get_protocol(self: *const URL) []const u8 {
- // std.Uri keeps a pointer to "https", "http" (scheme part) so we know
- // its followed by ':'.
- const scheme = self.uri.scheme;
- return scheme.ptr[0 .. scheme.len + 1];
+ pub fn get_username(self: *const URL) []const u8 {
+ const username = ada.getUsernameNullable(self.internal);
+ if (username.data == null) {
+ return "";
+ }
+
+ return username.data[0..username.length];
}
- pub fn get_username(self: *URL) []const u8 {
- return uriComponentNullStr(self.uri.user);
+ pub fn get_password(self: *const URL) []const u8 {
+ const password = ada.getPasswordNullable(self.internal);
+ if (password.data == null) {
+ return "";
+ }
+
+ return password.data[0..password.length];
}
- pub fn get_password(self: *URL) []const u8 {
- return uriComponentNullStr(self.uri.password);
+ pub fn get_port(self: *const URL) []const u8 {
+ const port = ada.getPortNullable(self.internal);
+ if (port.data == null) {
+ return "";
+ }
+
+ return port.data[0..port.length];
}
- pub fn get_host(self: *URL, page: *Page) ![]const u8 {
- var aw = std.Io.Writer.Allocating.init(page.arena);
- try self.uri.writeToStream(&aw.writer, .{
- .scheme = false,
- .authentication = false,
- .authority = true,
- .path = false,
- .query = false,
- .fragment = false,
- });
- return aw.written();
+ pub fn get_hash(self: *const URL) []const u8 {
+ const hash = ada.getHashNullable(self.internal);
+ if (hash.data == null) {
+ return "";
+ }
+
+ return hash.data[0..hash.length];
}
- pub fn get_hostname(self: *URL) []const u8 {
- return uriComponentNullStr(self.uri.host);
+ pub fn get_host(self: *const URL) []const u8 {
+ const host = ada.getHostNullable(self.internal);
+ if (host.data == null) {
+ return "";
+ }
+
+ return host.data[0..host.length];
}
- pub fn get_port(self: *URL, page: *Page) ![]const u8 {
- const arena = page.arena;
- if (self.uri.port == null) return try arena.dupe(u8, "");
+ pub fn get_hostname(self: *const URL) []const u8 {
+ const hostname = ada.getHostnameNullable(self.internal);
+ if (hostname.data == null) {
+ return "";
+ }
- var aw = std.Io.Writer.Allocating.init(arena);
- try aw.writer.printInt(self.uri.port.?, 10, .lower, .{});
- return aw.written();
+ return hostname.data[0..hostname.length];
}
- pub fn get_pathname(self: *URL) []const u8 {
- if (uriComponentStr(self.uri.path).len == 0) return "/";
- return uriComponentStr(self.uri.path);
+ pub fn get_pathname(self: *const URL) []const u8 {
+ const path = ada.getPathnameNullable(self.internal);
+ // Return a slash if path is null.
+ if (path.data == null) {
+ return "/";
+ }
+
+ return path.data[0..path.length];
}
- pub fn get_search(self: *URL, page: *Page) ![]const u8 {
+ /// get_search depends on the current state of `search_params`.
+ pub fn get_search(self: *const URL, page: *Page) ![]const u8 {
const arena = page.arena;
if (self.search_params.get_size() == 0) {
@@ -223,72 +266,104 @@ pub const URL = struct {
return buf.items;
}
- pub fn set_search(self: *URL, qs_: ?[]const u8, page: *Page) !void {
- self.search_params = .{};
- if (qs_) |qs| {
- self.search_params = try URLSearchParams.init(page.arena, qs);
+ pub fn get_protocol(self: *const URL) []const u8 {
+ const protocol = ada.getProtocolNullable(self.internal);
+ if (protocol.data == null) {
+ return "";
}
+
+ return protocol.data[0..protocol.length];
}
- pub fn get_hash(self: *URL, page: *Page) ![]const u8 {
- const arena = page.arena;
- if (self.uri.fragment == null) return try arena.dupe(u8, "");
+ // Setters.
+
+ /// Ada-url don't define any errors, so we just prefer one unified
+ /// `Internal` error for failing cases.
+ const SetterError = error{Internal};
- return try std.mem.concat(arena, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) });
+ pub fn set_href(self: *URL, input: []const u8, page: *Page) !void {
+ _ = ada.setHref(self.internal, input);
+ if (!ada.isValid(self.internal)) return error.Internal;
+ // Can't call `get_search` here since it uses `search_params`.
+ self.search_params = try prepareSearchParams(page.arena, self.internal);
}
- pub fn get_searchParams(self: *URL) *URLSearchParams {
- return &self.search_params;
+ pub fn set_host(self: *const URL, input: []const u8) SetterError!void {
+ _ = ada.setHost(self.internal, input);
+ if (!ada.isValid(self.internal)) return error.Internal;
}
- pub fn _toJSON(self: *URL, page: *Page) ![]const u8 {
- return self.get_href(page);
+ pub fn set_hostname(self: *const URL, input: []const u8) SetterError!void {
+ _ = ada.setHostname(self.internal, input);
+ if (!ada.isValid(self.internal)) return error.Internal;
+ }
+
+ pub fn set_protocol(self: *const URL, input: []const u8) SetterError!void {
+ _ = ada.setProtocol(self.internal, input);
+ if (!ada.isValid(self.internal)) return error.Internal;
}
-};
-// 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 "";
+ pub fn set_username(self: *const URL, input: []const u8) SetterError!void {
+ _ = ada.setUsername(self.internal, input);
+ if (!ada.isValid(self.internal)) return error.Internal;
+ }
- return uriComponentStr(c.?);
-}
+ pub fn set_password(self: *const URL, input: []const u8) SetterError!void {
+ _ = ada.setPassword(self.internal, input);
+ if (!ada.isValid(self.internal)) return error.Internal;
+ }
-fn uriComponentStr(c: std.Uri.Component) []const u8 {
- return switch (c) {
- .raw => |v| v,
- .percent_encoded => |v| v,
- };
-}
+ pub fn set_port(self: *const URL, input: []const u8) SetterError!void {
+ _ = ada.setPort(self.internal, input);
+ if (!ada.isValid(self.internal)) return error.Internal;
+ }
+
+ pub fn set_pathname(self: *const URL, input: []const u8) SetterError!void {
+ _ = ada.setPathname(self.internal, input);
+ if (!ada.isValid(self.internal)) return error.Internal;
+ }
+
+ pub fn set_search(self: *URL, maybe_input: ?[]const u8, page: *Page) !void {
+ self.search_params = .{};
+ if (maybe_input) |input| {
+ self.search_params = try .initFromString(page.arena, input);
+ }
+ }
+
+ pub fn set_hash(self: *const URL, input: []const u8) !void {
+ ada.setHash(self.internal, input);
+ if (!ada.isValid(self.internal)) return error.Internal;
+ }
+};
-// https://url.spec.whatwg.org/#interface-urlsearchparams
pub const URLSearchParams = struct {
entries: kv.List = .{},
- const URLSearchParamsOpts = union(enum) {
- qs: []const u8,
+ pub const ConstructorOptions = union(enum) {
+ query_string: []const u8,
form_data: *const FormData,
- js_obj: js.Object,
+ object: js.Object,
};
- 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 = .{};
+
+ pub fn constructor(maybe_options: ?ConstructorOptions, page: *Page) !URLSearchParams {
+ const options = maybe_options orelse return .{};
+
+ const arena = page.arena;
+ return switch (options) {
+ .query_string => |string| .{ .entries = try parseQuery(arena, string) },
+ .form_data => |form_data| .{ .entries = try form_data.entries.clone(arena) },
+ .object => |object| {
+ var it = object.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),
- );
+ const js_value = try object.get(name);
+ const value = try js_value.toString(arena);
+
+ entries.appendOwnedAssumeCapacity(name, value);
}
return .{ .entries = entries };
@@ -296,10 +371,9 @@ pub const URLSearchParams = struct {
};
}
- pub fn init(arena: Allocator, qs_: ?[]const u8) !URLSearchParams {
- return .{
- .entries = if (qs_) |qs| try parseQuery(arena, qs) else .{},
- };
+ /// Initializes URLSearchParams from a query string.
+ pub fn initFromString(arena: Allocator, query_string: []const u8) !URLSearchParams {
+ return .{ .entries = try parseQuery(arena, query_string) };
}
pub fn get_size(self: *const URLSearchParams) u32 {
diff --git a/src/tests/html/link.html b/src/tests/html/link.html
index 0f1d869bf..15da64611 100644
--- a/src/tests/html/link.html
+++ b/src/tests/html/link.html
@@ -16,8 +16,8 @@
testing.expectEqual('https://lightpanda.io', link.origin);
link.host = 'lightpanda.io:443';
- testing.expectEqual('lightpanda.io:443', link.host);
- testing.expectEqual('443', link.port);
+ testing.expectEqual('lightpanda.io', link.host);
+ testing.expectEqual('', link.port);
testing.expectEqual('lightpanda.io', link.hostname);
link.host = 'lightpanda.io';
@@ -42,9 +42,9 @@
testing.expectEqual('', link.port);
link.port = '443';
- testing.expectEqual('foo.bar:443', link.host);
+ testing.expectEqual('foo.bar', link.host);
testing.expectEqual('foo.bar', link.hostname);
- testing.expectEqual('https://foo.bar:443/?q=bar#frag', link.href);
+ testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
link.port = null;
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
diff --git a/src/tests/url/url.html b/src/tests/url/url.html
index f6b8301c9..9083bdd1e 100644
--- a/src/tests/url/url.html
+++ b/src/tests/url/url.html
@@ -64,6 +64,23 @@
testing.expectEqual(null, url.searchParams.get('a'));
+
+
diff --git a/src/url.zig b/src/url.zig
index acfac2560..a818327e1 100644
--- a/src/url.zig
+++ b/src/url.zig
@@ -75,10 +75,6 @@ pub const URL = struct {
return writer.writeAll(self.raw);
}
- pub fn toWebApi(self: *const URL, allocator: Allocator) !WebApiURL {
- return WebApiURL.init(allocator, self.uri);
- }
-
/// Properly stitches two URL fragments together.
///
/// For URLs with a path, it will replace the last entry with the src.
diff --git a/vendor/ada/root.zig b/vendor/ada/root.zig
new file mode 100644
index 000000000..da2810682
--- /dev/null
+++ b/vendor/ada/root.zig
@@ -0,0 +1,168 @@
+//! Wrappers for ada URL parser.
+//! https://github.com/ada-url/ada
+
+const c = @cImport({
+ @cInclude("ada_c.h");
+});
+
+pub const URLComponents = c.ada_url_components;
+pub const URLOmitted = c.ada_url_omitted;
+pub const String = c.ada_string;
+pub const OwnedString = c.ada_owned_string;
+/// Pointer types.
+pub const URL = c.ada_url;
+pub const URLSearchParams = c.ada_url_search_params;
+
+pub const ParseError = error{Invalid};
+
+pub fn parse(input: []const u8) ParseError!URL {
+ const url = c.ada_parse(input.ptr, input.len);
+ if (!c.ada_is_valid(url)) {
+ return error.Invalid;
+ }
+
+ return url;
+}
+
+pub fn parseWithBase(input: []const u8, base: []const u8) ParseError!URL {
+ const url = c.ada_parse_with_base(input.ptr, input.len, base.ptr, base.len);
+ if (!c.ada_is_valid(url)) {
+ return error.Invalid;
+ }
+
+ return url;
+}
+
+pub inline fn getComponents(url: URL) *const URLComponents {
+ return c.ada_get_components(url);
+}
+
+pub inline fn free(url: URL) void {
+ return c.ada_free(url);
+}
+
+pub inline fn freeOwnedString(owned: OwnedString) void {
+ return c.ada_free_owned_string(owned);
+}
+
+/// Returns true if given URL is valid.
+pub inline fn isValid(url: URL) bool {
+ return c.ada_is_valid(url);
+}
+
+/// Creates a new `URL` from given `URL`.
+pub inline fn copy(url: URL) URL {
+ return c.ada_copy(url);
+}
+
+/// Contrary to other getters, this heap allocates.
+pub inline fn getOriginNullable(url: URL) OwnedString {
+ return c.ada_get_origin(url);
+}
+
+pub inline fn getHrefNullable(url: URL) String {
+ return c.ada_get_href(url);
+}
+
+pub inline fn getUsernameNullable(url: URL) String {
+ return c.ada_get_username(url);
+}
+
+pub inline fn getPasswordNullable(url: URL) String {
+ return c.ada_get_password(url);
+}
+
+pub inline fn getSearchNullable(url: URL) String {
+ return c.ada_get_search(url);
+}
+
+pub inline fn getPortNullable(url: URL) String {
+ return c.ada_get_port(url);
+}
+
+pub inline fn getHashNullable(url: URL) String {
+ return c.ada_get_hash(url);
+}
+
+pub inline fn getHostNullable(url: URL) String {
+ return c.ada_get_host(url);
+}
+
+pub inline fn getHostnameNullable(url: URL) String {
+ return c.ada_get_hostname(url);
+}
+
+pub inline fn getPathnameNullable(url: URL) String {
+ return c.ada_get_pathname(url);
+}
+
+pub inline fn getProtocolNullable(url: URL) String {
+ return c.ada_get_protocol(url);
+}
+
+pub inline fn setHref(url: URL, input: []const u8) bool {
+ return c.ada_set_href(url, input.ptr, input.len);
+}
+
+pub inline fn setHost(url: URL, input: []const u8) bool {
+ return c.ada_set_host(url, input.ptr, input.len);
+}
+
+pub inline fn setHostname(url: URL, input: []const u8) bool {
+ return c.ada_set_hostname(url, input.ptr, input.len);
+}
+
+pub inline fn setProtocol(url: URL, input: []const u8) bool {
+ return c.ada_set_protocol(url, input.ptr, input.len);
+}
+
+pub inline fn setUsername(url: URL, input: []const u8) bool {
+ return c.ada_set_username(url, input.ptr, input.len);
+}
+
+pub inline fn setPassword(url: URL, input: []const u8) bool {
+ return c.ada_set_password(url, input.ptr, input.len);
+}
+
+pub inline fn setPort(url: URL, input: []const u8) bool {
+ return c.ada_set_port(url, input.ptr, input.len);
+}
+
+pub inline fn setPathname(url: URL, input: []const u8) bool {
+ return c.ada_set_pathname(url, input.ptr, input.len);
+}
+
+pub inline fn setSearch(url: URL, input: []const u8) void {
+ return c.ada_set_search(url, input.ptr, input.len);
+}
+
+pub inline fn setHash(url: URL, input: []const u8) void {
+ return c.ada_set_hash(url, input.ptr, input.len);
+}
+
+pub inline fn clearHash(url: URL) void {
+ return c.ada_clear_hash(url);
+}
+
+pub inline fn clearSearch(url: URL) void {
+ return c.ada_clear_search(url);
+}
+
+pub inline fn clearPort(url: URL) void {
+ return c.ada_clear_port(url);
+}
+
+pub const Scheme = struct {
+ pub const http: u8 = 0;
+ pub const not_special: u8 = 1;
+ pub const https: u8 = 2;
+ pub const ws: u8 = 3;
+ pub const ftp: u8 = 4;
+ pub const wss: u8 = 5;
+ pub const file: u8 = 6;
+};
+
+/// Returns one of the constants defined in `Scheme`.
+pub inline fn getSchemeType(url: URL) u8 {
+ return c.ada_get_scheme_type(url);
+}