From 27cadde1bc70872240bd3e489ce4ef927a583839 Mon Sep 17 00:00:00 2001 From: nikneym Date: Thu, 2 Oct 2025 18:23:30 +0300 Subject: [PATCH 1/6] integrate ada-url dependency to build system --- build.zig | 25 +++++++++++++++++++++++++ build.zig.zon | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/build.zig b/build.zig index 3437dfad0..9d4594fb4 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,27 @@ 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 = b.addLibrary(.{ + .name = "ada", + .linkage = .static, + .root_module = b.createModule(.{ + .target = m.resolved_target, + .optimize = m.optimize, + .link_libcpp = true, + }), + }); + + // Expose "ada_c.h". + ada.installHeader(ada_dep.path("ada_c.h"), "ada_c.h"); + + ada.root_module.addCSourceFiles(.{ + .root = ada_dep.path(""), + .files = &.{"ada.cpp"}, + .flags = &.{"-std=c++20"}, + }); + + m.linkLibrary(ada); +} diff --git a/build.zig.zon b/build.zig.zon index 27cc26ec7..f9d0ccaac 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,5 +9,9 @@ .hash = "v8-0.0.0-xddH6yTGAwA__kfiDozsVXL2_jnycrtDUfR8kIADQFUz", }, //.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", + }, }, } From 366bfb9dcca1f6f0b445bee5f6de2fee75d8387d Mon Sep 17 00:00:00 2001 From: nikneym Date: Sun, 5 Oct 2025 11:02:05 +0300 Subject: [PATCH 2/6] add ada-url wrappers * also integrate it as module in build.zig rather than direct linking --- build.zig | 26 +++++------ vendor/ada/root.zig | 103 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 vendor/ada/root.zig diff --git a/build.zig b/build.zig index 9d4594fb4..61667069c 100644 --- a/build.zig +++ b/build.zig @@ -853,24 +853,24 @@ 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 = b.addLibrary(.{ - .name = "ada", - .linkage = .static, - .root_module = b.createModule(.{ - .target = m.resolved_target, - .optimize = m.optimize, - .link_libcpp = true, - }), + const dep_root = ada_dep.path(""); + + // Private module that binds ada functions. + const ada_mod = b.createModule(.{ + .root_source_file = b.path("vendor/ada/root.zig"), + .target = m.resolved_target, + .optimize = m.optimize, + .link_libcpp = true, }); - // Expose "ada_c.h". - ada.installHeader(ada_dep.path("ada_c.h"), "ada_c.h"); + // Expose headers; note that "ada.h" is a C++ header so no use here. + ada_mod.addIncludePath(dep_root); - ada.root_module.addCSourceFiles(.{ - .root = ada_dep.path(""), + ada_mod.addCSourceFiles(.{ + .root = dep_root, .files = &.{"ada.cpp"}, .flags = &.{"-std=c++20"}, }); - m.linkLibrary(ada); + m.addImport("ada", ada_mod); } diff --git a/vendor/ada/root.zig b/vendor/ada/root.zig new file mode 100644 index 000000000..4deaec699 --- /dev/null +++ b/vendor/ada/root.zig @@ -0,0 +1,103 @@ +//! Wrappers for ada URL parser. +//! https://github.com/ada-url/ada + +const c = @cImport({ + @cInclude("ada_c.h"); +}); + +/// Pointer type. +pub const URL = c.ada_url; +pub const String = c.ada_string; +pub const OwnedString = c.ada_owned_string; +/// Pointer type. +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)) { + free(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)) { + free(url); + return error.Invalid; + } + + return 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); +} + +/// Can return an empty string. +/// Contrary to other getters, returned slice is heap allocated. +pub inline fn getOrigin(url: URL) []const u8 { + const origin = c.ada_get_origin(url); + return origin.data[0..origin.length]; +} + +/// Can return an empty string. +pub inline fn getHref(url: URL) []const u8 { + const href = c.ada_get_href(url); + return href.data[0..href.length]; +} + +/// Can return an empty string. +pub inline fn getUsername(url: URL) []const u8 { + const username = c.ada_get_username(url); + return username.data[0..username.length]; +} + +/// Can return an empty string. +pub inline fn getPassword(url: URL) []const u8 { + const password = c.ada_get_password(url); + return password.data[0..password.length]; +} + +pub inline fn getPort(url: URL) []const u8 { + const port = c.ada_get_port(url); + return port.data[0..port.length]; +} + +pub inline fn getHash(url: URL) []const u8 { + const hash = c.ada_get_hash(url); + return hash.data[0..hash.length]; +} + +pub inline fn getHost(url: URL) []const u8 { + const host = c.ada_get_host(url); + return host.data[0..host.length]; +} + +pub inline fn getHostname(url: URL) []const u8 { + const hostname = c.ada_get_hostname(url); + return hostname.data[0..hostname.length]; +} + +pub inline fn getPathname(url: URL) []const u8 { + const pathname = c.ada_get_pathname(url); + return pathname.data[0..pathname.length]; +} + +pub inline fn getSearch(url: URL) []const u8 { + const search = c.ada_get_search(url); + return search.data[0..search.length]; +} + +pub inline fn getProtocol(url: URL) []const u8 { + const protocol = c.ada_get_protocol(url); + return protocol.data[0..protocol.length]; +} From f1af2b932f89dc271448e7044fcb6c0d0028b080 Mon Sep 17 00:00:00 2001 From: nikneym Date: Sun, 5 Oct 2025 11:02:53 +0300 Subject: [PATCH 3/6] initial `URL` refactor --- src/browser/url/url.zig | 328 ++++++++-------------------------------- 1 file changed, 63 insertions(+), 265 deletions(-) diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig index 8cc744785..c7db0c052 100644 --- a/src/browser/url/url.zig +++ b/src/browser/url/url.zig @@ -18,6 +18,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const ada = @import("ada"); const js = @import("../js/js.zig"); const parser = @import("../netsurf.zig"); @@ -35,208 +36,111 @@ 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, - search_params: URLSearchParams, + internal: ada.URL, - pub const empty = URL{ - .uri = .{ .scheme = "" }, - .search_params = .{}, - }; - - const URLArg = union(enum) { + // You can use an existing URL object for either argument, and it will be + // stringified from the object's href property. + pub const ConstructorArg = union(enum) { url: *URL, - element: *parser.ElementHTML, + element: *parser.Element, 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"), - } + fn toString(self: *const ConstructorArg) error{Invalid}![]const u8 { + return switch (self) { + .string => |s| s, + .url => |url| url._toString(), + .element => |e| parser.elementGetAttribute(@ptrCast(e), "href") orelse error.Invalid, + }; } }; - 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; - - 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, .{}); + pub fn constructor(url: ConstructorArg, maybe_base: ?ConstructorArg, _: *Page) !URL { + const u = blk: { + const url_str = try url.toString(); + if (maybe_base) |base| { + break :blk ada.parseWithBase(url_str, try base.toString()); } - } - 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); - } - - 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 init(arena, uri); - } - - pub fn init(arena: Allocator, uri: std.Uri) !URL { - return .{ - .uri = uri, - .search_params = try URLSearchParams.init( - arena, - uriComponentNullStr(uri.query), - ), + break :blk ada.parse(url_str); }; - } - 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(); + return .{ .url = u }; } - // 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 destructor(self: *const URL) void { + ada.free(self.internal); } - pub fn _toString(self: *URL, page: *Page) ![]const u8 { - return self.toString(page.arena); + // Alias to get_href. + pub fn _toString(self: *const URL) []const u8 { + return ada.getHref(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, - }); - - if (self.search_params.get_size() > 0) { - try aw.writer.writeByte('?'); - try self.search_params.write(&aw.writer); - } + // Getters. - { - const fragment = uriComponentNullStr(self.uri.fragment); - if (fragment.len > 0) { - try aw.writer.writeByte('#'); - try aw.writer.writeAll(fragment); - } - } + pub fn get_origin(self: *const URL, page: *Page) ![]const u8 { + const arena = page.arena; + // `ada.getOrigin` 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 origin = ada.getOrigin(self.internal); + // `OwnedString` itself is not heap allocated so this is safe. + defer ada.freeOwnedString(.{ .data = origin.ptr, .length = origin.len }); - return aw.written(); + return arena.dupe(u8, origin); } - pub fn get_protocol(self: *URL, page: *Page) ![]const u8 { - return try std.mem.concat(page.arena, u8, &[_][]const u8{ self.uri.scheme, ":" }); + pub fn get_href(self: *const URL) []const u8 { + return ada.getHref(self.internal); } - pub fn get_username(self: *URL) []const u8 { - return uriComponentNullStr(self.uri.user); + pub fn get_username(self: *const URL) []const u8 { + return ada.getUsername(self.internal); } - pub fn get_password(self: *URL) []const u8 { - return uriComponentNullStr(self.uri.password); + pub fn get_password(self: *const URL) []const u8 { + return ada.getPassword(self.internal); } - 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_port(self: *const URL) []const u8 { + return ada.getPort(self.internal); } - pub fn get_hostname(self: *URL) []const u8 { - return uriComponentNullStr(self.uri.host); + pub fn get_hash(self: *const URL) []const u8 { + return ada.getHash(self.internal); } - pub fn get_port(self: *URL, page: *Page) ![]const u8 { - const arena = page.arena; - if (self.uri.port == null) return try arena.dupe(u8, ""); - - var aw = std.Io.Writer.Allocating.init(arena); - try aw.writer.printInt(self.uri.port.?, 10, .lower, .{}); - return aw.written(); + pub fn get_host(self: *const URL) []const u8 { + return ada.getHost(self.internal); } - pub fn get_pathname(self: *URL) []const u8 { - if (uriComponentStr(self.uri.path).len == 0) return "/"; - return uriComponentStr(self.uri.path); + pub fn get_hostname(self: *const URL) []const u8 { + return ada.getHostname(self.internal); } - pub fn get_search(self: *URL, page: *Page) ![]const u8 { - const arena = page.arena; - - if (self.search_params.get_size() == 0) { - return ""; - } - - var buf: std.ArrayListUnmanaged(u8) = .{}; - try buf.append(arena, '?'); - try self.search_params.encode(buf.writer(arena)); - return buf.items; + pub fn get_pathname(self: *const URL) []const u8 { + return ada.getPathname(self.internal); } - 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_search(self: *const URL) []const u8 { + return ada.getSearch(self.internal); } - pub fn get_hash(self: *URL, page: *Page) ![]const u8 { - const arena = page.arena; - if (self.uri.fragment == null) return try arena.dupe(u8, ""); - - return try std.mem.concat(arena, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) }); + pub fn get_protocol(self: *const URL) []const u8 { + return ada.getProtocol(self.internal); } +}; - pub fn get_searchParams(self: *URL) *URLSearchParams { - return &self.search_params; - } +pub const URLSearchParams = struct { + internal: ada.URLSearchParams, - pub fn _toJSON(self: *URL, page: *Page) ![]const u8 { - return self.get_href(page); - } + pub const ConstructorOptions = union(enum) { + string: []const u8, + form_data: *const FormData, + object: js.JsObject, + }; }; // uriComponentNullStr converts an optional std.Uri.Component to string value. @@ -254,112 +158,6 @@ fn uriComponentStr(c: std.Uri.Component) []const u8 { }; } -// https://url.spec.whatwg.org/#interface-urlsearchparams -pub const URLSearchParams = struct { - entries: kv.List = .{}, - - const URLSearchParamsOpts = union(enum) { - qs: []const u8, - form_data: *const FormData, - js_obj: 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 = .{}; - 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 }; - }, - }; - } - - pub fn init(arena: Allocator, qs_: ?[]const u8) !URLSearchParams { - return .{ - .entries = if (qs_) |qs| try parseQuery(arena, qs) else .{}, - }; - } - - pub fn get_size(self: *const URLSearchParams) u32 { - return @intCast(self.entries.count()); - } - - pub fn _append(self: *URLSearchParams, name: []const u8, value: []const u8, page: *Page) !void { - return self.entries.append(page.arena, name, value); - } - - pub fn _set(self: *URLSearchParams, name: []const u8, value: []const u8, page: *Page) !void { - return self.entries.set(page.arena, name, value); - } - - pub fn _delete(self: *URLSearchParams, name: []const u8, value_: ?[]const u8) void { - if (value_) |value| { - return self.entries.deleteKeyValue(name, value); - } - return self.entries.delete(name); - } - - pub fn _get(self: *const URLSearchParams, name: []const u8) ?[]const u8 { - return self.entries.get(name); - } - - pub fn _getAll(self: *const URLSearchParams, name: []const u8, page: *Page) ![]const []const u8 { - return self.entries.getAll(page.call_arena, name); - } - - pub fn _has(self: *const URLSearchParams, name: []const u8) bool { - return self.entries.has(name); - } - - pub fn _keys(self: *const URLSearchParams) KeyIterable { - return .{ .inner = self.entries.keyIterator() }; - } - - pub fn _values(self: *const URLSearchParams) ValueIterable { - return .{ .inner = self.entries.valueIterator() }; - } - - pub fn _entries(self: *const URLSearchParams) EntryIterable { - return .{ .inner = self.entries.entryIterator() }; - } - - pub fn _symbol_iterator(self: *const URLSearchParams) EntryIterable { - return self._entries(); - } - - pub fn _toString(self: *const URLSearchParams, page: *Page) ![]const u8 { - var arr: std.ArrayListUnmanaged(u8) = .empty; - try self.write(arr.writer(page.call_arena)); - return arr.items; - } - - fn write(self: *const URLSearchParams, writer: anytype) !void { - return kv.urlEncode(self.entries, .query, writer); - } - - // TODO - pub fn _sort(_: *URLSearchParams) void {} - - fn encode(self: *const URLSearchParams, writer: anytype) !void { - return kv.urlEncode(self.entries, .query, writer); - } -}; - // Parse the given query. fn parseQuery(arena: Allocator, s: []const u8) !kv.List { var list = kv.List{}; From 526a28523ed9ea674d13ed67f7f1e41b33ce6762 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 7 Oct 2025 17:11:51 +0300 Subject: [PATCH 4/6] bind more ada-url functions --- vendor/ada/root.zig | 73 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/vendor/ada/root.zig b/vendor/ada/root.zig index 4deaec699..64d48b83b 100644 --- a/vendor/ada/root.zig +++ b/vendor/ada/root.zig @@ -7,6 +7,8 @@ const c = @cImport({ /// Pointer type. pub const URL = c.ada_url; +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 type. @@ -34,6 +36,10 @@ pub fn parseWithBase(input: []const u8, base: []const u8) ParseError!URL { 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); } @@ -42,6 +48,11 @@ pub inline fn freeOwnedString(owned: OwnedString) void { return c.ada_free_owned_string(owned); } +/// Returns true if given URL is valid (not NULL). +pub inline fn isValid(url: URL) bool { + return c.ada_is_valid(url); +} + /// Can return an empty string. /// Contrary to other getters, returned slice is heap allocated. pub inline fn getOrigin(url: URL) []const u8 { @@ -68,6 +79,10 @@ pub inline fn getPassword(url: URL) []const u8 { } pub inline fn getPort(url: URL) []const u8 { + if (!c.ada_has_port(url)) { + return ""; + } + const port = c.ada_get_port(url); return port.data[0..port.length]; } @@ -77,12 +92,21 @@ pub inline fn getHash(url: URL) []const u8 { return hash.data[0..hash.length]; } +/// Returns an empty string if not provided. pub inline fn getHost(url: URL) []const u8 { const host = c.ada_get_host(url); + if (host.data == null) { + return ""; + } + return host.data[0..host.length]; } pub inline fn getHostname(url: URL) []const u8 { + if (!c.ada_has_hostname(url)) { + return ""; + } + const hostname = c.ada_get_hostname(url); return hostname.data[0..hostname.length]; } @@ -92,12 +116,55 @@ pub inline fn getPathname(url: URL) []const u8 { return pathname.data[0..pathname.length]; } -pub inline fn getSearch(url: URL) []const u8 { - const search = c.ada_get_search(url); - return search.data[0..search.length]; +pub inline fn getSearch(url: URL) String { + return c.ada_get_search(url); } pub inline fn getProtocol(url: URL) []const u8 { const protocol = c.ada_get_protocol(url); return protocol.data[0..protocol.length]; } + +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 clearSearch(url: URL) void { + return c.ada_clear_search(url); +} From 092f6a692f83c556c67e93abbc02b6b7f8d3f18e Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 7 Oct 2025 17:18:08 +0300 Subject: [PATCH 5/6] basic url parsing working * also reintroduces old `URLSearchParams` implementation since ada prefers its own iterator where we'd like to use our own. --- src/browser/html/document.zig | 4 +- src/browser/html/elements.zig | 101 ++++++------- src/browser/html/location.zig | 18 +-- src/browser/url/url.zig | 257 +++++++++++++++++++++++++++++----- 4 files changed, 274 insertions(+), 106 deletions(-) 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 0fc5502f6..27559a4a9 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -275,54 +275,53 @@ pub const HTMLAnchorElement = struct { // 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); + 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(page); + return u.get_protocol(); } pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void { const arena = page.arena; + _ = arena; var u = try url(self, page); - u.uri.scheme = v; - const href = try u.toString(arena); + u.set_protocol(v); + const href = try u.get_href(page); try 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); + return u.get_host(); } pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void { // search : separator - var p: ?u16 = null; + var p: ?[]const u8 = 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); + //p = try std.fmt.parseInt(u16, v[i + 1 ..], 10); + p = v[i + 1 ..]; break; } } - const arena = page.arena; var u = try url(self, page); - if (p) |pp| { - u.uri.host = .{ .raw = h }; - u.uri.port = pp; + if (p) |port| { + u.set_host(h); + u.set_port(port); } else { - u.uri.host = .{ .raw = v }; - u.uri.port = null; + u.set_host(v); } - const href = try u.toString(arena); + const href = try u.get_href(page); try parser.anchorSetHref(self, href); } @@ -332,30 +331,26 @@ pub const HTMLAnchorElement = struct { } pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void { - const arena = page.arena; var u = try url(self, page); - u.uri.host = .{ .raw = v }; - const href = try u.toString(arena); + u.set_host(v); + const href = try u.get_href(page); try 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); + return u.get_port(); } pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { - const arena = page.arena; var u = try url(self, page); if (v != null and v.?.len > 0) { - u.uri.port = try std.fmt.parseInt(u16, v.?, 10); - } else { - u.uri.port = null; + u.set_host(v.?); } - const href = try u.toString(arena); + const href = try u.get_href(page); try parser.anchorSetHref(self, href); } @@ -366,37 +361,28 @@ pub const HTMLAnchorElement = struct { } pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { - const arena = page.arena; - var u = try url(self, page); + if (v) |username| { + var u = try url(self, page); + u.set_username(username); - if (v) |vv| { - u.uri.user = .{ .raw = vv }; - } else { - u.uri.user = null; + const href = try u.get_href(page); + try parser.anchorSetHref(self, href); } - const href = try u.toString(arena); - - try 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()); + return u.get_password(); } pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { - const arena = page.arena; - var u = try url(self, page); + if (v) |password| { + var u = try url(self, page); + u.set_password(password); - if (v) |vv| { - u.uri.password = .{ .raw = vv }; - } else { - u.uri.password = null; + const href = try u.get_href(page); + try parser.anchorSetHref(self, href); } - const href = try u.toString(arena); - - try parser.anchorSetHref(self, href); } // TODO return a disposable string @@ -406,44 +392,35 @@ pub const HTMLAnchorElement = struct { } pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void { - const arena = page.arena; var u = try url(self, page); - u.uri.path = .{ .raw = v }; - const href = try u.toString(arena); - + u.set_pathname(v); + const href = try u.get_href(page); try 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); + return u.get_search(page); } - pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { + pub fn set_search(self: *parser.Anchor, v: []const u8, page: *Page) !void { var u = try url(self, page); try u.set_search(v, page); - const href = try u.toString(page.call_arena); + const href = try u.get_href(page); try 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); + return 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, v: []const u8, page: *Page) !void { var u = try url(self, page); - - if (v) |vv| { - u.uri.fragment = .{ .raw = vv }; - } else { - u.uri.fragment = null; - } - const href = try u.toString(arena); - + u.set_hash(v); + const href = try u.get_href(page); try parser.anchorSetHref(self, href); } }; diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig index 99b1e93cd..959f60de1 100644 --- a/src/browser/html/location.zig +++ b/src/browser/html/location.zig @@ -29,13 +29,13 @@ pub const Location = struct { return ""; } - pub fn get_protocol(self: *Location, page: *Page) ![]const u8 { - if (self.url) |*u| return u.get_protocol(page); + pub fn get_protocol(self: *Location) []const u8 { + if (self.url) |*u| return u.get_protocol(); return ""; } - pub fn get_host(self: *Location, page: *Page) ![]const u8 { - if (self.url) |*u| return u.get_host(page); + pub fn get_host(self: *Location) []const u8 { + if (self.url) |*u| return u.get_host(); return ""; } @@ -44,8 +44,8 @@ pub const Location = struct { return ""; } - pub fn get_port(self: *Location, page: *Page) ![]const u8 { - if (self.url) |*u| return u.get_port(page); + pub fn get_port(self: *Location) []const u8 { + if (self.url) |*u| return u.get_port(); return ""; } @@ -59,8 +59,8 @@ pub const Location = struct { return ""; } - pub fn get_hash(self: *Location, page: *Page) ![]const u8 { - if (self.url) |*u| return u.get_hash(page); + pub fn get_hash(self: *Location) []const u8 { + if (self.url) |*u| return u.get_hash(); return ""; } @@ -82,7 +82,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/url/url.zig b/src/browser/url/url.zig index c7db0c052..1c064e1cf 100644 --- a/src/browser/url/url.zig +++ b/src/browser/url/url.zig @@ -18,6 +18,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const Writer = std.Io.Writer; const ada = @import("ada"); const js = @import("../js/js.zig"); @@ -39,47 +40,82 @@ pub const Interfaces = .{ /// https://developer.mozilla.org/en-US/docs/Web/API/URL/URL pub const URL = struct { 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{ + .internal = null, + .search_params = .{}, + }; // You can use an existing URL object for either argument, and it will be // stringified from the object's href property. - pub const ConstructorArg = union(enum) { - url: *URL, - element: *parser.Element, + const ConstructorArg = union(enum) { string: []const u8, + url: *const URL, + element: *parser.Element, - fn toString(self: *const ConstructorArg) error{Invalid}![]const u8 { + fn toString(self: ConstructorArg, page: *Page) ![]const u8 { return switch (self) { .string => |s| s, - .url => |url| url._toString(), - .element => |e| parser.elementGetAttribute(@ptrCast(e), "href") orelse error.Invalid, + .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: ConstructorArg, maybe_base: ?ConstructorArg, _: *Page) !URL { - const u = blk: { - const url_str = try url.toString(); + pub fn constructor(url: ConstructorArg, maybe_base: ?ConstructorArg, page: *Page) !URL { + const url_str = try url.toString(page); + + const internal = try blk: { if (maybe_base) |base| { - break :blk ada.parseWithBase(url_str, try base.toString()); + break :blk ada.parseWithBase(url_str, try base.toString(page)); } break :blk ada.parse(url_str); }; - return .{ .url = u }; + // Prepare search_params. + const params: URLSearchParams = blk: { + const search = ada.getSearch(internal); + if (search.data == null) { + break :blk .{}; + } + + break :blk try .initFromString(page.arena, search.data[0..search.length]); + }; + + // We're doing this since we track search params separately. + ada.clearSearch(internal); + + return .{ .internal = internal, .search_params = params }; } pub fn destructor(self: *const URL) void { - ada.free(self.internal); + // Not tracked by arena. + return ada.free(self.internal); } // Alias to get_href. - pub fn _toString(self: *const URL) []const u8 { - return ada.getHref(self.internal); + pub fn _toString(self: *const URL, page: *Page) ![]const u8 { + return self.get_href(page); } // Getters. + pub fn get_searchParams(self: *URL) *URLSearchParams { + return &self.search_params; + } + pub fn get_origin(self: *const URL, page: *Page) ![]const u8 { const arena = page.arena; // `ada.getOrigin` allocates memory in order to find the `origin`. @@ -92,8 +128,27 @@ pub const URL = struct { return arena.dupe(u8, origin); } - pub fn get_href(self: *const URL) []const u8 { - return ada.getHref(self.internal); + pub fn get_href(self: *const URL, page: *Page) ![]const u8 { + var w: Writer.Allocating = .init(page.arena); + + const href = ada.getHref(self.internal); + const comps = ada.getComponents(self.internal); + 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_username(self: *const URL) []const u8 { @@ -124,39 +179,175 @@ pub const URL = struct { return ada.getPathname(self.internal); } - pub fn get_search(self: *const URL) []const u8 { - return ada.getSearch(self.internal); + // 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) { + return ""; + } + + var buf: std.ArrayListUnmanaged(u8) = .{}; + try buf.append(arena, '?'); + try self.search_params.encode(buf.writer(arena)); + return buf.items; } pub fn get_protocol(self: *const URL) []const u8 { return ada.getProtocol(self.internal); } + + // Setters. + + // FIXME: reinit search_params? + pub fn set_href(self: *const URL, input: []const u8) void { + _ = ada.setHref(self.internal, input); + } + + pub fn set_host(self: *const URL, input: []const u8) void { + _ = ada.setHost(self.internal, input); + } + + pub fn set_hostname(self: *const URL, input: []const u8) void { + _ = ada.setHostname(self.internal, input); + } + + pub fn set_protocol(self: *const URL, input: []const u8) void { + _ = ada.setProtocol(self.internal, input); + } + + pub fn set_username(self: *const URL, input: []const u8) void { + _ = ada.setUsername(self.internal, input); + } + + pub fn set_password(self: *const URL, input: []const u8) void { + _ = ada.setPassword(self.internal, input); + } + + pub fn set_port(self: *const URL, input: []const u8) void { + _ = ada.setPort(self.internal, input); + } + + pub fn set_pathname(self: *const URL, input: []const u8) void { + _ = ada.setPathname(self.internal, input); + } + + 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); + } }; pub const URLSearchParams = struct { - internal: ada.URLSearchParams, + entries: kv.List = .{}, pub const ConstructorOptions = union(enum) { - string: []const u8, + query_string: []const u8, form_data: *const FormData, object: js.JsObject, }; -}; -// 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 constructor(maybe_options: ?ConstructorOptions, page: *Page) !URLSearchParams { + const options = maybe_options orelse return .{}; - return uriComponentStr(c.?); -} + 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(); -fn uriComponentStr(c: std.Uri.Component) []const u8 { - return switch (c) { - .raw => |v| v, - .percent_encoded => |v| v, - }; -} + 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_value = try object.get(name); + const value = try js_value.toString(arena); + + entries.appendOwnedAssumeCapacity(name, value); + } + + return .{ .entries = entries }; + }, + }; + } + + /// 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 { + return @intCast(self.entries.count()); + } + + pub fn _append(self: *URLSearchParams, name: []const u8, value: []const u8, page: *Page) !void { + return self.entries.append(page.arena, name, value); + } + + pub fn _set(self: *URLSearchParams, name: []const u8, value: []const u8, page: *Page) !void { + return self.entries.set(page.arena, name, value); + } + + pub fn _delete(self: *URLSearchParams, name: []const u8, value_: ?[]const u8) void { + if (value_) |value| { + return self.entries.deleteKeyValue(name, value); + } + return self.entries.delete(name); + } + + pub fn _get(self: *const URLSearchParams, name: []const u8) ?[]const u8 { + return self.entries.get(name); + } + + pub fn _getAll(self: *const URLSearchParams, name: []const u8, page: *Page) ![]const []const u8 { + return self.entries.getAll(page.call_arena, name); + } + + pub fn _has(self: *const URLSearchParams, name: []const u8) bool { + return self.entries.has(name); + } + + pub fn _keys(self: *const URLSearchParams) KeyIterable { + return .{ .inner = self.entries.keyIterator() }; + } + + pub fn _values(self: *const URLSearchParams) ValueIterable { + return .{ .inner = self.entries.valueIterator() }; + } + + pub fn _entries(self: *const URLSearchParams) EntryIterable { + return .{ .inner = self.entries.entryIterator() }; + } + + pub fn _symbol_iterator(self: *const URLSearchParams) EntryIterable { + return self._entries(); + } + + pub fn _toString(self: *const URLSearchParams, page: *Page) ![]const u8 { + var arr: std.ArrayListUnmanaged(u8) = .empty; + try self.write(arr.writer(page.call_arena)); + return arr.items; + } + + fn write(self: *const URLSearchParams, writer: anytype) !void { + return kv.urlEncode(self.entries, .query, writer); + } + + // TODO + pub fn _sort(_: *URLSearchParams) void {} + + fn encode(self: *const URLSearchParams, writer: anytype) !void { + return kv.urlEncode(self.entries, .query, writer); + } +}; // Parse the given query. fn parseQuery(arena: Allocator, s: []const u8) !kv.List { From 8cde0f0ee0205e7f689588b0e1da21446979c1dc Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Thu, 9 Oct 2025 10:12:00 +0300 Subject: [PATCH 6/6] change after rebase --- src/browser/url/url.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig index 1c064e1cf..c79ced8ba 100644 --- a/src/browser/url/url.zig +++ b/src/browser/url/url.zig @@ -250,7 +250,7 @@ pub const URLSearchParams = struct { pub const ConstructorOptions = union(enum) { query_string: []const u8, form_data: *const FormData, - object: js.JsObject, + object: js.Object, }; pub fn constructor(maybe_options: ?ConstructorOptions, page: *Page) !URLSearchParams {