From 0e3f18367ad5837871fdad7e344dcd9e55b386e5 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Sun, 2 Nov 2025 21:04:11 -0800 Subject: [PATCH 1/7] add set_hash to Location --- src/browser/html/location.zig | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig index 649883de8..31be5a6b4 100644 --- a/src/browser/html/location.zig +++ b/src/browser/html/location.zig @@ -16,7 +16,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -const Uri = @import("std").Uri; +const std = @import("std"); +const Uri = std.Uri; const Page = @import("../page.zig").Page; const URL = @import("../url/url.zig").URL; @@ -42,6 +43,20 @@ pub const Location = struct { return page.navigateFromWebAPI(href, .{ .reason = .script }, .{ .push = null }); } + 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] + else + current_url; + + 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); + } + pub fn get_protocol(self: *Location) []const u8 { return self.url.get_protocol(); } From c009669ec8682dceb10bf2b327ac8d98a520a227 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Sun, 2 Nov 2025 21:07:50 -0800 Subject: [PATCH 2/7] properly handle replace navigation case --- src/browser/navigation/Navigation.zig | 62 +++++++++++++++++++-------- src/browser/page.zig | 4 +- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/browser/navigation/Navigation.zig b/src/browser/navigation/Navigation.zig index 13b009e23..8f700ecf0 100644 --- a/src/browser/navigation/Navigation.zig +++ b/src/browser/navigation/Navigation.zig @@ -101,28 +101,27 @@ pub fn _forward(self: *Navigation, page: *Page) !NavigationReturn { return self.navigate(next_entry.url, .{ .traverse = new_index }, page); } +pub fn updateEntries(self: *Navigation, url: []const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void { + switch (kind) { + .replace => { + _ = try self.replaceEntry(url, null, page, dispatch); + }, + .push => |state| { + _ = try self.pushEntry(url, state, page, dispatch); + }, + .traverse => |index| { + self.index = index; + }, + .reload => {}, + } +} + // This is for after true navigation processing, where we need to ensure that our entries are up to date. // This is only really safe to run in the `pageDoneCallback` where we can guarantee that the URL and NavigationKind are correct. pub fn processNavigation(self: *Navigation, page: *Page) !void { const url = page.url.raw; - const kind = page.session.navigation_kind; - - if (kind) |k| { - switch (k) { - .replace => { - // When replacing, we just update the URL but the state is nullified. - const entry = self.currentEntry(); - entry.url = url; - entry.state = null; - }, - .push => |state| { - _ = try self.pushEntry(url, state, page, false); - }, - .traverse, .reload => {}, - } - } else { - _ = try self.pushEntry(url, null, page, false); - } + const kind: NavigationKind = page.session.navigation_kind orelse .{ .push = null }; + try self.updateEntries(url, kind, page, false); } /// Pushes an entry into the Navigation stack WITHOUT actually navigating to it. @@ -166,6 +165,33 @@ pub fn pushEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page: return entry; } +pub fn replaceEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page: *Page, dispatch: bool) !*NavigationHistoryEntry { + const arena = page.session.arena; + const url = try arena.dupe(u8, _url); + + const previous = self.currentEntry(); + + const id = self.next_entry_id; + self.next_entry_id += 1; + const id_str = try std.fmt.allocPrint(arena, "{d}", .{id}); + + const entry = try arena.create(NavigationHistoryEntry); + entry.* = NavigationHistoryEntry{ + .id = id_str, + .key = id_str, + .url = url, + .state = state, + }; + + self.entries.items[self.index] = entry; + + if (dispatch) { + NavigationCurrentEntryChangeEvent.dispatch(self, previous, .replace); + } + + return entry; +} + const NavigateOptions = struct { const NavigateOptionsHistory = enum { pub const ENUM_JS_USE_TAG = true; diff --git a/src/browser/page.zig b/src/browser/page.zig index b7eb7c159..579dd6d5e 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -1075,9 +1075,7 @@ pub const Page = struct { if (try self.url.eqlDocument(&new_url, session.transfer_arena)) { self.url = new_url; - - const prev = session.navigation.currentEntry(); - NavigationCurrentEntryChangeEvent.dispatch(&self.session.navigation, prev, kind); + try session.navigation.updateEntries(stitched_url, kind, self, true); return; } } From 3cc53b579b97c017e69dfe9dc7af56443fc0dee4 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 3 Nov 2025 07:01:50 -0800 Subject: [PATCH 3/7] add location set hash tests --- src/browser/html/location.zig | 2 +- src/tests/html/location.html | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig index 31be5a6b4..0e12f933b 100644 --- a/src/browser/html/location.zig +++ b/src/browser/html/location.zig @@ -52,7 +52,7 @@ pub const Location = struct { current_url; const normalized_hash = std.mem.trimStart(u8, hash, "#"); - const new_url = try std.fmt.allocPrint(page.arena, "{s}#{s}", .{ base_without_hash, normalized_hash }); + const new_url = try std.fmt.allocPrint(page.session.transfer_arena, "{s}#{s}", .{ base_without_hash, normalized_hash }); return page.navigateFromWebAPI(new_url, .{ .reason = .script }, .replace); } diff --git a/src/tests/html/location.html b/src/tests/html/location.html index c39ecb6fc..863ae6587 100644 --- a/src/tests/html/location.html +++ b/src/tests/html/location.html @@ -13,3 +13,13 @@ testing.expectEqual("9582", location.port); testing.expectEqual("", location.search); + + From 38c6a9bd9d363b0dbb32b4f822f4d41024012cb2 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 3 Nov 2025 09:16:49 -0800 Subject: [PATCH 4/7] changeLocation on nav --- src/browser/html/location.zig | 2 +- src/browser/page.zig | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig index 0e12f933b..31be5a6b4 100644 --- a/src/browser/html/location.zig +++ b/src/browser/html/location.zig @@ -52,7 +52,7 @@ pub const Location = struct { current_url; const normalized_hash = std.mem.trimStart(u8, hash, "#"); - const new_url = try std.fmt.allocPrint(page.session.transfer_arena, "{s}#{s}", .{ base_without_hash, normalized_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); } diff --git a/src/browser/page.zig b/src/browser/page.zig index 579dd6d5e..195fc2514 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -1065,7 +1065,15 @@ pub const Page = struct { // specifically for this type of lifetime. pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts, kind: NavigationKind) !void { const session = self.session; - const stitched_url = try URL.stitch(session.transfer_arena, url, self.url.raw, .{ .alloc = .always }); + const stitched_url = try URL.stitch( + session.transfer_arena, + url, + self.url.raw, + .{ + .alloc = .always, + .null_terminated = true, + }, + ); // Force will force a page load. // Otherwise, we need to check if this is a true navigation. @@ -1075,6 +1083,8 @@ 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; } From 92ddb5640dbff999526410c3268b0b1cc49cab10 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 5 Nov 2025 08:35:28 -0800 Subject: [PATCH 5/7] new NavigationEventTarget on new page --- src/browser/navigation/Navigation.zig | 6 ++++++ src/browser/session.zig | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/browser/navigation/Navigation.zig b/src/browser/navigation/Navigation.zig index 8f700ecf0..ceba17e6b 100644 --- a/src/browser/navigation/Navigation.zig +++ b/src/browser/navigation/Navigation.zig @@ -47,6 +47,12 @@ index: usize = 0, entries: std.ArrayListUnmanaged(*NavigationHistoryEntry) = .empty, next_entry_id: usize = 0, +pub fn resetForNewPage(self: *Navigation) void { + // libdom will automatically clean this up when a new page is made. + // We must create a new target whenever we create a new page. + self.proto = NavigationEventTarget{}; +} + pub fn get_canGoBack(self: *const Navigation) bool { return self.index > 0; } diff --git a/src/browser/session.zig b/src/browser/session.zig index 394e508bf..0bb941aba 100644 --- a/src/browser/session.zig +++ b/src/browser/session.zig @@ -104,6 +104,9 @@ pub const Session = struct { // We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded parser.init(); + // creates a new event target for Navigation + self.navigation.resetForNewPage(); + const page_arena = &self.browser.page_arena; _ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); _ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 1024 }); From 19b9ba86012c303efdf8ef4eab74b389027a002f Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 5 Nov 2025 10:07:28 -0800 Subject: [PATCH 6/7] 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 31be5a6b4..27dcd335b 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; - - const normalized_hash = std.mem.trimStart(u8, hash, "#"); - const new_url = try std.fmt.allocPrint(page.arena, "{s}#{s}", .{ base_without_hash, normalized_hash }); + try std.fmt.allocPrint(page.arena, "#{s}", .{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 ceba17e6b..3c6bfcfd9 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 195fc2514..65348281b 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 bb8292fa6..b9c829908 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 (raw_base.len == 0 or isCompleteHTTPUrl(trimmed_path)) { + return simpleStitch(allocator, trimmed_path, opts); + } - if (base.len == 0 or isCompleteHTTPUrl(path)) { - return simpleStitch(allocator, 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; From 16e7c0841dc425c2ce51eb07413a6377d4dc99a1 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 10 Nov 2025 06:52:14 -0800 Subject: [PATCH 7/7] handle empty hashes in Location --- src/browser/html/location.zig | 19 +++++++++++++------ src/tests/html/location.html | 8 ++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig index 27dcd335b..458178457 100644 --- a/src/browser/html/location.zig +++ b/src/browser/html/location.zig @@ -17,8 +17,6 @@ // along with this program. If not, see . const std = @import("std"); -const Uri = std.Uri; - const Page = @import("../page.zig").Page; const URL = @import("../url/url.zig").URL; @@ -44,10 +42,19 @@ pub const Location = struct { } pub fn set_hash(_: *const Location, hash: []const u8, page: *Page) !void { - const normalized_hash = if (hash[0] == '#') - hash - else - try std.fmt.allocPrint(page.arena, "#{s}", .{hash}); + const normalized_hash = blk: { + if (hash.len == 0) { + const old_url = page.url.raw; + + break :blk if (std.mem.indexOfScalar(u8, old_url, '#')) |index| + old_url[0..index] + else + old_url; + } else if (hash[0] == '#') + break :blk hash + else + break :blk try std.fmt.allocPrint(page.arena, "#{s}", .{hash}); + }; return page.navigateFromWebAPI(normalized_hash, .{ .reason = .script }, .replace); } diff --git a/src/tests/html/location.html b/src/tests/html/location.html index 863ae6587..a5de3ba86 100644 --- a/src/tests/html/location.html +++ b/src/tests/html/location.html @@ -15,6 +15,10 @@