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;