From 19b9ba86012c303efdf8ef4eab74b389027a002f Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 5 Nov 2025 10:07:28 -0800 Subject: [PATCH] add hash support to URL stitch --- src/browser/html/location.zig | 13 ++---- src/browser/navigation/Navigation.zig | 4 +- src/browser/page.zig | 1 - src/url.zig | 58 +++++++++++++++++++-------- 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig index 31be5a6b..27dcd335 100644 --- a/src/browser/html/location.zig +++ b/src/browser/html/location.zig @@ -44,17 +44,12 @@ pub const Location = struct { } pub fn set_hash(_: *const Location, hash: []const u8, page: *Page) !void { - const current_url = page.url.raw; - - const base_without_hash = if (std.mem.indexOfScalar(u8, current_url, '#')) |pos| - current_url[0..pos] + const normalized_hash = if (hash[0] == '#') + hash else - current_url; + try std.fmt.allocPrint(page.arena, "#{s}", .{hash}); - const normalized_hash = std.mem.trimStart(u8, hash, "#"); - const new_url = try std.fmt.allocPrint(page.arena, "{s}#{s}", .{ base_without_hash, normalized_hash }); - - return page.navigateFromWebAPI(new_url, .{ .reason = .script }, .replace); + return page.navigateFromWebAPI(normalized_hash, .{ .reason = .script }, .replace); } pub fn get_protocol(self: *Location) []const u8 { diff --git a/src/browser/navigation/Navigation.zig b/src/browser/navigation/Navigation.zig index ceba17e6..3c6bfcfd 100644 --- a/src/browser/navigation/Navigation.zig +++ b/src/browser/navigation/Navigation.zig @@ -228,7 +228,9 @@ pub fn navigate( const committed = try page.js.createPromiseResolver(.page); const finished = try page.js.createPromiseResolver(.page); - const new_url = try URL.parse(url, null); + const new_url_string = try URL.stitch(arena, url, page.url.raw, .{}); + const new_url = try URL.parse(new_url_string, null); + const is_same_document = try page.url.eqlDocument(&new_url, arena); switch (kind) { diff --git a/src/browser/page.zig b/src/browser/page.zig index 195fc251..65348281 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -1084,7 +1084,6 @@ pub const Page = struct { if (try self.url.eqlDocument(&new_url, session.transfer_arena)) { self.url = new_url; try self.window.changeLocation(self.url.raw, self); - try session.navigation.updateEntries(stitched_url, kind, self, true); return; } diff --git a/src/url.zig b/src/url.zig index bb8292fa..b9c82990 100644 --- a/src/url.zig +++ b/src/url.zig @@ -82,17 +82,35 @@ pub const URL = struct { pub fn stitch( allocator: Allocator, raw_path: []const u8, - base: []const u8, + raw_base: []const u8, comptime opts: StitchOpts, ) !StitchReturn(opts) { - const path = std.mem.trim(u8, raw_path, &.{ '\n', '\r' }); + const trimmed_path = std.mem.trim(u8, raw_path, &.{ '\n', '\r' }); - if (base.len == 0 or isCompleteHTTPUrl(path)) { - return simpleStitch(allocator, path, opts); + if (raw_base.len == 0 or isCompleteHTTPUrl(trimmed_path)) { + return simpleStitch(allocator, trimmed_path, opts); } + if (trimmed_path.len == 0) { + return simpleStitch(allocator, raw_base, opts); + } + + // base should get stripped of its hash whenever we are stitching. + const base = if (std.mem.indexOfScalar(u8, raw_base, '#')) |hash_pos| + raw_base[0..hash_pos] + else + raw_base; + + const path_hash_start = std.mem.indexOfScalar(u8, trimmed_path, '#'); + const path = if (path_hash_start) |pos| trimmed_path[0..pos] else trimmed_path; + const hash = if (path_hash_start) |pos| trimmed_path[pos..] else ""; + + // if path is just hash, we just append it to base. if (path.len == 0) { - return simpleStitch(allocator, base, opts); + if (comptime opts.null_terminated) { + return std.fmt.allocPrintSentinel(allocator, "{s}{s}", .{ base, hash }, 0); + } + return std.fmt.allocPrint(allocator, "{s}{s}", .{ base, hash }); } if (std.mem.startsWith(u8, path, "//")) { @@ -103,9 +121,9 @@ pub const URL = struct { const protocol = base[0..index]; if (comptime opts.null_terminated) { - return std.fmt.allocPrintSentinel(allocator, "{s}:{s}", .{ protocol, path }, 0); + return std.fmt.allocPrintSentinel(allocator, "{s}:{s}{s}", .{ protocol, path, hash }, 0); } - return std.fmt.allocPrint(allocator, "{s}:{s}", .{ protocol, path }); + return std.fmt.allocPrint(allocator, "{s}:{s}{s}", .{ protocol, path, hash }); } // Quick hack because domains have to be at least 3 characters. @@ -126,25 +144,28 @@ pub const URL = struct { return std.fmt.allocPrint(allocator, "{s}{s}", .{ root, path }); } - var old_path = std.mem.trimStart(u8, base[root.len..], "/"); - if (std.mem.lastIndexOfScalar(u8, old_path, '/')) |pos| { - old_path = old_path[0..pos]; + var oldraw_path = std.mem.trimStart(u8, base[root.len..], "/"); + if (std.mem.lastIndexOfScalar(u8, oldraw_path, '/')) |pos| { + oldraw_path = oldraw_path[0..pos]; } else { - old_path = ""; + oldraw_path = ""; } // We preallocate all of the space possibly needed. - // This is the root, old_path, new path, 3 slashes and perhaps a null terminated slot. - var out = try allocator.alloc(u8, root.len + old_path.len + path.len + 3 + if (comptime opts.null_terminated) 1 else 0); + // This is the root, oldraw_path, new path, 3 slashes and perhaps a null terminated slot. + var out = try allocator.alloc( + u8, + root.len + oldraw_path.len + path.len + hash.len + 3 + if (comptime opts.null_terminated) 1 else 0, + ); var end: usize = 0; @memmove(out[0..root.len], root); end += root.len; out[root.len] = '/'; end += 1; // If we don't have an old path, do nothing here. - if (old_path.len > 0) { - @memmove(out[end .. end + old_path.len], old_path); - end += old_path.len; + if (oldraw_path.len > 0) { + @memmove(out[end .. end + oldraw_path.len], oldraw_path); + end += oldraw_path.len; out[end] = '/'; end += 1; } @@ -182,6 +203,11 @@ pub const URL = struct { read += 1; } + if (hash.len > 0) { + @memmove(out[write .. write + hash.len], hash); + write += hash.len; + } + if (comptime opts.null_terminated) { // we always have an extra space out[write] = 0;