Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/browser/html/location.zig
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

const Uri = @import("std").Uri;

const std = @import("std");
const Page = @import("../page.zig").Page;
const URL = @import("../url/url.zig").URL;

Expand All @@ -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();
}
Expand Down
72 changes: 53 additions & 19 deletions src/browser/navigation/Navigation.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
15 changes: 11 additions & 4 deletions src/browser/page.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/browser/session.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
18 changes: 18 additions & 0 deletions src/tests/html/location.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,21 @@
testing.expectEqual("9582", location.port);
testing.expectEqual("", location.search);
</script>

<script id=location_hash>
location.hash = "";
testing.expectEqual("", location.hash);
testing.expectEqual('http://localhost:9582/src/tests/html/location.html', location.href);

location.hash = "#abcdef";
testing.expectEqual("#abcdef", location.hash);
testing.expectEqual('http://localhost:9582/src/tests/html/location.html#abcdef', location.href);

location.hash = "xyzxyz";
testing.expectEqual("#xyzxyz", location.hash);
testing.expectEqual('http://localhost:9582/src/tests/html/location.html#xyzxyz', location.href);

location.hash = "";
testing.expectEqual("", location.hash);
testing.expectEqual('http://localhost:9582/src/tests/html/location.html', location.href);
</script>
58 changes: 42 additions & 16 deletions src/url.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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, "//")) {
Expand All @@ -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.
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down