diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig index 649883de8..458178457 100644 --- a/src/browser/html/location.zig +++ b/src/browser/html/location.zig @@ -16,8 +16,7 @@ // 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 Page = @import("../page.zig").Page; const URL = @import("../url/url.zig").URL; @@ -42,6 +41,24 @@ 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 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); + } + pub fn get_protocol(self: *Location) []const u8 { return self.url.get_protocol(); } diff --git a/src/browser/navigation/Navigation.zig b/src/browser/navigation/Navigation.zig index 13b009e23..3c6bfcfd9 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; } @@ -101,28 +107,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 +171,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; @@ -196,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 b7eb7c159..65348281b 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,9 +1083,8 @@ 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 self.window.changeLocation(self.url.raw, self); + try session.navigation.updateEntries(stitched_url, kind, self, true); return; } } 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 }); diff --git a/src/tests/html/location.html b/src/tests/html/location.html index c39ecb6fc..a5de3ba86 100644 --- a/src/tests/html/location.html +++ b/src/tests/html/location.html @@ -13,3 +13,21 @@ testing.expectEqual("9582", location.port); testing.expectEqual("", location.search); + + 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;