From 9deb5249a97f48c7471d3d373dac527d83c8615d Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 21 Oct 2025 16:42:01 +0300 Subject: [PATCH 01/12] introduce ada-url to build system Also add ada-url bindings. --- build.zig | 32 +++++++++ build.zig.zon | 4 ++ vendor/ada/root.zig | 168 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 vendor/ada/root.zig 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/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); +} From c568a75599cb544ec513926909747e1b047a143f Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 21 Oct 2025 16:43:09 +0300 Subject: [PATCH 02/12] refactor `URL` web API --- src/browser/url/url.zig | 420 +++++++++++++++++++++++----------------- 1 file changed, 247 insertions(+), 173 deletions(-) diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig index 08c97bf61..d40771efe 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,224 @@ 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 = .{} }; + } + + /// 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 initWithoutSearchParams(uri: std.Uri) URL { - return .{ .uri = uri, .search_params = .{} }; + /// 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; } - 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(); + pub fn clearPort(self: *const URL) void { + return ada.clearPort(self.internal); } - // 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 clearHash(self: *const URL) void { + return ada.clearHash(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, page: *Page) ![]const u8 { + return self.get_href(page); } - // 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, - }); + // Getters. - if (self.search_params.get_size() > 0) { - try aw.writer.writeByte('?'); - try self.search_params.write(&aw.writer); + 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.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 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 +267,103 @@ 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) }); + // FIXME: reinit search_params? + pub fn set_href(self: *const URL, input: []const u8) SetterError!void { + _ = ada.setHref(self.internal, input); + if (!ada.isValid(self.internal)) return error.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 { From 69884b9d8d7e0f4555c9c5969f17282d25120c27 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 21 Oct 2025 16:44:29 +0300 Subject: [PATCH 03/12] `Location` changes regarding to changes in `URL` --- src/browser/html/document.zig | 4 ++-- src/browser/html/location.zig | 21 +++++++++++---------- src/browser/html/window.zig | 7 ++++++- src/browser/page.zig | 2 +- src/url.zig | 4 ---- 5 files changed, 20 insertions(+), 18 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/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/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. From 8342f0c3948d4c77528839261efc73921653b860 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 21 Oct 2025 16:46:06 +0300 Subject: [PATCH 04/12] omit `try` keyword when not necessary --- src/browser/html/elements.zig | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 9673db692..5c9d5c67e 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 { From ba66b7c5db1f08ae752a58d8a5547481fe83b586 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 21 Oct 2025 16:49:12 +0300 Subject: [PATCH 05/12] refactor `HTMLAnchorElement` regarding to `URL` changes This still doesn't use `state` since `state` doesn't allow us to iterate the nodes when releasing the memory and we need to call `URL.destructor` when freeing. In the future, we might omit getter allocations by making such change. --- src/browser/html/elements.zig | 185 +++++++++++++++++----------------- 1 file changed, 93 insertions(+), 92 deletions(-) diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 5c9d5c67e..46de1542c 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -269,182 +269,183 @@ 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.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.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.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.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.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.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.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.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); } }; From d60d3ebaac8015c4f92e299d48202c7e5d22ee41 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 21 Oct 2025 16:49:48 +0300 Subject: [PATCH 06/12] update `link.html` test --- src/tests/html/link.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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); From 7d39bc979f308ecbb43e661b531ea8cdfaafd6c3 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 21 Oct 2025 16:50:16 +0300 Subject: [PATCH 07/12] remove `invalidUrl` test in `url.html` --- src/tests/url/url.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/tests/url/url.html b/src/tests/url/url.html index f6b8301c9..a28a980a7 100644 --- a/src/tests/url/url.html +++ b/src/tests/url/url.html @@ -76,8 +76,3 @@ testing.expectEqual("", sk.hostname); testing.expectEqual("sveltekit-internal://", sk.href); - - From d4d35670a0f6f69a7a20320c9eef334200f28619 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 22 Oct 2025 11:42:16 +0300 Subject: [PATCH 08/12] prefer `call_arena` in web APIs --- src/browser/html/elements.zig | 16 ++++++++-------- src/browser/url/url.zig | 3 +-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 46de1542c..c5756f54d 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -284,7 +284,7 @@ pub const HTMLAnchorElement = struct { var u = try url(self, page); defer u.destructor(); - return page.arena.dupe(u8, u.get_protocol()); + return page.call_arena.dupe(u8, u.get_protocol()); } pub fn set_protocol(self: *parser.Anchor, protocol: []const u8, page: *Page) !void { @@ -301,7 +301,7 @@ pub const HTMLAnchorElement = struct { var u = url(self, page) catch return ""; defer u.destructor(); - return page.arena.dupe(u8, u.get_host()); + return page.call_arena.dupe(u8, u.get_host()); } pub fn set_host(self: *parser.Anchor, host: []const u8, page: *Page) !void { @@ -316,7 +316,7 @@ pub const HTMLAnchorElement = struct { pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 { var u = url(self, page) catch return ""; defer u.destructor(); - return page.arena.dupe(u8, u.get_hostname()); + return page.call_arena.dupe(u8, u.get_hostname()); } pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void { @@ -332,7 +332,7 @@ pub const HTMLAnchorElement = struct { pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 { var u = url(self, page) catch return ""; defer u.destructor(); - return page.arena.dupe(u8, u.get_port()); + return page.call_arena.dupe(u8, u.get_port()); } pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void { @@ -359,7 +359,7 @@ pub const HTMLAnchorElement = struct { return ""; } - return page.arena.dupe(u8, username); + return page.call_arena.dupe(u8, username); } pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void { @@ -378,7 +378,7 @@ pub const HTMLAnchorElement = struct { var u = url(self, page) catch return ""; defer u.destructor(); - return page.arena.dupe(u8, u.get_password()); + return page.call_arena.dupe(u8, u.get_password()); } pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void { @@ -397,7 +397,7 @@ pub const HTMLAnchorElement = struct { var u = url(self, page) catch return ""; defer u.destructor(); - return page.arena.dupe(u8, u.get_pathname()); + return page.call_arena.dupe(u8, u.get_pathname()); } pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void { @@ -431,7 +431,7 @@ pub const HTMLAnchorElement = struct { var u = url(self, page) catch return ""; defer u.destructor(); - return page.arena.dupe(u8, u.get_hash()); + return page.call_arena.dupe(u8, u.get_hash()); } pub fn set_hash(self: *parser.Anchor, maybe_hash: ?[]const u8, page: *Page) !void { diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig index d40771efe..b05feacec 100644 --- a/src/browser/url/url.zig +++ b/src/browser/url/url.zig @@ -144,7 +144,6 @@ pub const URL = struct { } pub fn get_origin(self: *const URL, page: *Page) ![]const u8 { - const arena = page.arena; // `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. @@ -155,7 +154,7 @@ pub const URL = struct { defer ada.freeOwnedString(maybe_origin); const origin = maybe_origin.data[0..maybe_origin.length]; - return arena.dupe(u8, origin); + return page.call_arena.dupe(u8, origin); } pub fn get_href(self: *const URL, page: *Page) ![]const u8 { From 2d14452ddae4ca2f720f169a4a6acdbf28f3d3fa Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 22 Oct 2025 13:40:44 +0300 Subject: [PATCH 09/12] remove stale todo comments --- src/browser/html/elements.zig | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index c5756f54d..8667c04f5 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -272,14 +272,12 @@ pub const HTMLAnchorElement = struct { 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); 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); defer u.destructor(); @@ -296,7 +294,6 @@ pub const HTMLAnchorElement = struct { return parser.anchorSetHref(self, href); } - // TODO return a disposable string pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 { var u = url(self, page) catch return ""; defer u.destructor(); @@ -328,7 +325,6 @@ pub const HTMLAnchorElement = struct { return parser.anchorSetHref(self, href); } - // TODO return a disposable string pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 { var u = url(self, page) catch return ""; defer u.destructor(); @@ -349,7 +345,6 @@ pub const HTMLAnchorElement = struct { return parser.anchorSetHref(self, href); } - // TODO return a disposable string pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 { var u = url(self, page) catch return ""; defer u.destructor(); @@ -373,7 +368,6 @@ pub const HTMLAnchorElement = struct { return parser.anchorSetHref(self, href); } - // TODO return a disposable string pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 { var u = url(self, page) catch return ""; defer u.destructor(); @@ -392,7 +386,6 @@ pub const HTMLAnchorElement = struct { return parser.anchorSetHref(self, href); } - // TODO return a disposable string pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 { var u = url(self, page) catch return ""; defer u.destructor(); @@ -426,7 +419,6 @@ pub const HTMLAnchorElement = struct { return parser.anchorSetHref(self, href); } - // TODO return a disposable string pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 { var u = url(self, page) catch return ""; defer u.destructor(); From 033eb82ae572c2a3dd3c20ead669a8cce0711e4e Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 22 Oct 2025 15:01:27 +0300 Subject: [PATCH 10/12] reinitialize `search_params` too when `href` set --- src/browser/url/url.zig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig index b05feacec..9229a23df 100644 --- a/src/browser/url/url.zig +++ b/src/browser/url/url.zig @@ -281,10 +281,11 @@ pub const URL = struct { /// `Internal` error for failing cases. const SetterError = error{Internal}; - // FIXME: reinit search_params? - pub fn set_href(self: *const URL, input: []const u8) SetterError!void { + 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 set_host(self: *const URL, input: []const u8) SetterError!void { From c6a0368c61f3b35872bcc2a652d820d28e3db213 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 22 Oct 2025 15:02:48 +0300 Subject: [PATCH 11/12] add a `searchParamsSetHref` test according to href setter change --- src/tests/url/url.html | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/tests/url/url.html b/src/tests/url/url.html index a28a980a7..43cd382ed 100644 --- a/src/tests/url/url.html +++ b/src/tests/url/url.html @@ -64,6 +64,23 @@ testing.expectEqual(null, url.searchParams.get('a')); + + + +