Skip to content

Commit 43b210d

Browse files
authored
Merge pull request #1200 from lightpanda-io/location-set-hash
add `set_hash` to Location
2 parents 2f2870c + 16e7c08 commit 43b210d

File tree

6 files changed

+146
-41
lines changed

6 files changed

+146
-41
lines changed

src/browser/html/location.zig

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
// You should have received a copy of the GNU Affero General Public License
1717
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1818

19-
const Uri = @import("std").Uri;
20-
19+
const std = @import("std");
2120
const Page = @import("../page.zig").Page;
2221
const URL = @import("../url/url.zig").URL;
2322

@@ -42,6 +41,24 @@ pub const Location = struct {
4241
return page.navigateFromWebAPI(href, .{ .reason = .script }, .{ .push = null });
4342
}
4443

44+
pub fn set_hash(_: *const Location, hash: []const u8, page: *Page) !void {
45+
const normalized_hash = blk: {
46+
if (hash.len == 0) {
47+
const old_url = page.url.raw;
48+
49+
break :blk if (std.mem.indexOfScalar(u8, old_url, '#')) |index|
50+
old_url[0..index]
51+
else
52+
old_url;
53+
} else if (hash[0] == '#')
54+
break :blk hash
55+
else
56+
break :blk try std.fmt.allocPrint(page.arena, "#{s}", .{hash});
57+
};
58+
59+
return page.navigateFromWebAPI(normalized_hash, .{ .reason = .script }, .replace);
60+
}
61+
4562
pub fn get_protocol(self: *Location) []const u8 {
4663
return self.url.get_protocol();
4764
}

src/browser/navigation/Navigation.zig

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ index: usize = 0,
4747
entries: std.ArrayListUnmanaged(*NavigationHistoryEntry) = .empty,
4848
next_entry_id: usize = 0,
4949

50+
pub fn resetForNewPage(self: *Navigation) void {
51+
// libdom will automatically clean this up when a new page is made.
52+
// We must create a new target whenever we create a new page.
53+
self.proto = NavigationEventTarget{};
54+
}
55+
5056
pub fn get_canGoBack(self: *const Navigation) bool {
5157
return self.index > 0;
5258
}
@@ -101,28 +107,27 @@ pub fn _forward(self: *Navigation, page: *Page) !NavigationReturn {
101107
return self.navigate(next_entry.url, .{ .traverse = new_index }, page);
102108
}
103109

110+
pub fn updateEntries(self: *Navigation, url: []const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void {
111+
switch (kind) {
112+
.replace => {
113+
_ = try self.replaceEntry(url, null, page, dispatch);
114+
},
115+
.push => |state| {
116+
_ = try self.pushEntry(url, state, page, dispatch);
117+
},
118+
.traverse => |index| {
119+
self.index = index;
120+
},
121+
.reload => {},
122+
}
123+
}
124+
104125
// This is for after true navigation processing, where we need to ensure that our entries are up to date.
105126
// This is only really safe to run in the `pageDoneCallback` where we can guarantee that the URL and NavigationKind are correct.
106127
pub fn processNavigation(self: *Navigation, page: *Page) !void {
107128
const url = page.url.raw;
108-
const kind = page.session.navigation_kind;
109-
110-
if (kind) |k| {
111-
switch (k) {
112-
.replace => {
113-
// When replacing, we just update the URL but the state is nullified.
114-
const entry = self.currentEntry();
115-
entry.url = url;
116-
entry.state = null;
117-
},
118-
.push => |state| {
119-
_ = try self.pushEntry(url, state, page, false);
120-
},
121-
.traverse, .reload => {},
122-
}
123-
} else {
124-
_ = try self.pushEntry(url, null, page, false);
125-
}
129+
const kind: NavigationKind = page.session.navigation_kind orelse .{ .push = null };
130+
try self.updateEntries(url, kind, page, false);
126131
}
127132

128133
/// 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:
166171
return entry;
167172
}
168173

174+
pub fn replaceEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page: *Page, dispatch: bool) !*NavigationHistoryEntry {
175+
const arena = page.session.arena;
176+
const url = try arena.dupe(u8, _url);
177+
178+
const previous = self.currentEntry();
179+
180+
const id = self.next_entry_id;
181+
self.next_entry_id += 1;
182+
const id_str = try std.fmt.allocPrint(arena, "{d}", .{id});
183+
184+
const entry = try arena.create(NavigationHistoryEntry);
185+
entry.* = NavigationHistoryEntry{
186+
.id = id_str,
187+
.key = id_str,
188+
.url = url,
189+
.state = state,
190+
};
191+
192+
self.entries.items[self.index] = entry;
193+
194+
if (dispatch) {
195+
NavigationCurrentEntryChangeEvent.dispatch(self, previous, .replace);
196+
}
197+
198+
return entry;
199+
}
200+
169201
const NavigateOptions = struct {
170202
const NavigateOptionsHistory = enum {
171203
pub const ENUM_JS_USE_TAG = true;
@@ -196,7 +228,9 @@ pub fn navigate(
196228
const committed = try page.js.createPromiseResolver(.page);
197229
const finished = try page.js.createPromiseResolver(.page);
198230

199-
const new_url = try URL.parse(url, null);
231+
const new_url_string = try URL.stitch(arena, url, page.url.raw, .{});
232+
const new_url = try URL.parse(new_url_string, null);
233+
200234
const is_same_document = try page.url.eqlDocument(&new_url, arena);
201235

202236
switch (kind) {

src/browser/page.zig

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,7 +1065,15 @@ pub const Page = struct {
10651065
// specifically for this type of lifetime.
10661066
pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts, kind: NavigationKind) !void {
10671067
const session = self.session;
1068-
const stitched_url = try URL.stitch(session.transfer_arena, url, self.url.raw, .{ .alloc = .always });
1068+
const stitched_url = try URL.stitch(
1069+
session.transfer_arena,
1070+
url,
1071+
self.url.raw,
1072+
.{
1073+
.alloc = .always,
1074+
.null_terminated = true,
1075+
},
1076+
);
10691077

10701078
// Force will force a page load.
10711079
// Otherwise, we need to check if this is a true navigation.
@@ -1075,9 +1083,8 @@ pub const Page = struct {
10751083

10761084
if (try self.url.eqlDocument(&new_url, session.transfer_arena)) {
10771085
self.url = new_url;
1078-
1079-
const prev = session.navigation.currentEntry();
1080-
NavigationCurrentEntryChangeEvent.dispatch(&self.session.navigation, prev, kind);
1086+
try self.window.changeLocation(self.url.raw, self);
1087+
try session.navigation.updateEntries(stitched_url, kind, self, true);
10811088
return;
10821089
}
10831090
}

src/browser/session.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ pub const Session = struct {
104104
// We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded
105105
parser.init();
106106

107+
// creates a new event target for Navigation
108+
self.navigation.resetForNewPage();
109+
107110
const page_arena = &self.browser.page_arena;
108111
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
109112
_ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 1024 });

src/tests/html/location.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,21 @@
1313
testing.expectEqual("9582", location.port);
1414
testing.expectEqual("", location.search);
1515
</script>
16+
17+
<script id=location_hash>
18+
location.hash = "";
19+
testing.expectEqual("", location.hash);
20+
testing.expectEqual('http://localhost:9582/src/tests/html/location.html', location.href);
21+
22+
location.hash = "#abcdef";
23+
testing.expectEqual("#abcdef", location.hash);
24+
testing.expectEqual('http://localhost:9582/src/tests/html/location.html#abcdef', location.href);
25+
26+
location.hash = "xyzxyz";
27+
testing.expectEqual("#xyzxyz", location.hash);
28+
testing.expectEqual('http://localhost:9582/src/tests/html/location.html#xyzxyz', location.href);
29+
30+
location.hash = "";
31+
testing.expectEqual("", location.hash);
32+
testing.expectEqual('http://localhost:9582/src/tests/html/location.html', location.href);
33+
</script>

src/url.zig

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,35 @@ pub const URL = struct {
8282
pub fn stitch(
8383
allocator: Allocator,
8484
raw_path: []const u8,
85-
base: []const u8,
85+
raw_base: []const u8,
8686
comptime opts: StitchOpts,
8787
) !StitchReturn(opts) {
88-
const path = std.mem.trim(u8, raw_path, &.{ '\n', '\r' });
88+
const trimmed_path = std.mem.trim(u8, raw_path, &.{ '\n', '\r' });
89+
90+
if (raw_base.len == 0 or isCompleteHTTPUrl(trimmed_path)) {
91+
return simpleStitch(allocator, trimmed_path, opts);
92+
}
8993

90-
if (base.len == 0 or isCompleteHTTPUrl(path)) {
91-
return simpleStitch(allocator, path, opts);
94+
if (trimmed_path.len == 0) {
95+
return simpleStitch(allocator, raw_base, opts);
9296
}
9397

98+
// base should get stripped of its hash whenever we are stitching.
99+
const base = if (std.mem.indexOfScalar(u8, raw_base, '#')) |hash_pos|
100+
raw_base[0..hash_pos]
101+
else
102+
raw_base;
103+
104+
const path_hash_start = std.mem.indexOfScalar(u8, trimmed_path, '#');
105+
const path = if (path_hash_start) |pos| trimmed_path[0..pos] else trimmed_path;
106+
const hash = if (path_hash_start) |pos| trimmed_path[pos..] else "";
107+
108+
// if path is just hash, we just append it to base.
94109
if (path.len == 0) {
95-
return simpleStitch(allocator, base, opts);
110+
if (comptime opts.null_terminated) {
111+
return std.fmt.allocPrintSentinel(allocator, "{s}{s}", .{ base, hash }, 0);
112+
}
113+
return std.fmt.allocPrint(allocator, "{s}{s}", .{ base, hash });
96114
}
97115

98116
if (std.mem.startsWith(u8, path, "//")) {
@@ -103,9 +121,9 @@ pub const URL = struct {
103121

104122
const protocol = base[0..index];
105123
if (comptime opts.null_terminated) {
106-
return std.fmt.allocPrintSentinel(allocator, "{s}:{s}", .{ protocol, path }, 0);
124+
return std.fmt.allocPrintSentinel(allocator, "{s}:{s}{s}", .{ protocol, path, hash }, 0);
107125
}
108-
return std.fmt.allocPrint(allocator, "{s}:{s}", .{ protocol, path });
126+
return std.fmt.allocPrint(allocator, "{s}:{s}{s}", .{ protocol, path, hash });
109127
}
110128

111129
// Quick hack because domains have to be at least 3 characters.
@@ -126,25 +144,28 @@ pub const URL = struct {
126144
return std.fmt.allocPrint(allocator, "{s}{s}", .{ root, path });
127145
}
128146

129-
var old_path = std.mem.trimStart(u8, base[root.len..], "/");
130-
if (std.mem.lastIndexOfScalar(u8, old_path, '/')) |pos| {
131-
old_path = old_path[0..pos];
147+
var oldraw_path = std.mem.trimStart(u8, base[root.len..], "/");
148+
if (std.mem.lastIndexOfScalar(u8, oldraw_path, '/')) |pos| {
149+
oldraw_path = oldraw_path[0..pos];
132150
} else {
133-
old_path = "";
151+
oldraw_path = "";
134152
}
135153

136154
// We preallocate all of the space possibly needed.
137-
// This is the root, old_path, new path, 3 slashes and perhaps a null terminated slot.
138-
var out = try allocator.alloc(u8, root.len + old_path.len + path.len + 3 + if (comptime opts.null_terminated) 1 else 0);
155+
// This is the root, oldraw_path, new path, 3 slashes and perhaps a null terminated slot.
156+
var out = try allocator.alloc(
157+
u8,
158+
root.len + oldraw_path.len + path.len + hash.len + 3 + if (comptime opts.null_terminated) 1 else 0,
159+
);
139160
var end: usize = 0;
140161
@memmove(out[0..root.len], root);
141162
end += root.len;
142163
out[root.len] = '/';
143164
end += 1;
144165
// If we don't have an old path, do nothing here.
145-
if (old_path.len > 0) {
146-
@memmove(out[end .. end + old_path.len], old_path);
147-
end += old_path.len;
166+
if (oldraw_path.len > 0) {
167+
@memmove(out[end .. end + oldraw_path.len], oldraw_path);
168+
end += oldraw_path.len;
148169
out[end] = '/';
149170
end += 1;
150171
}
@@ -182,6 +203,11 @@ pub const URL = struct {
182203
read += 1;
183204
}
184205

206+
if (hash.len > 0) {
207+
@memmove(out[write .. write + hash.len], hash);
208+
write += hash.len;
209+
}
210+
185211
if (comptime opts.null_terminated) {
186212
// we always have an extra space
187213
out[write] = 0;

0 commit comments

Comments
 (0)