Skip to content

Commit a579977

Browse files
Merge pull request #1086 from lightpanda-io/history
Implement `History` WebAPI.
2 parents f236a65 + 9992bd0 commit a579977

File tree

14 files changed

+317
-119
lines changed

14 files changed

+317
-119
lines changed

src/browser/events/event.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const MouseEvent = @import("mouse_event.zig").MouseEvent;
3636
const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
3737
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
3838
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
39+
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
3940

4041
// Event interfaces
4142
pub const Interfaces = .{
@@ -46,6 +47,7 @@ pub const Interfaces = .{
4647
KeyboardEvent,
4748
ErrorEvent,
4849
MessageEvent,
50+
PopStateEvent,
4951
};
5052

5153
pub const Union = generate.Union(Interfaces);
@@ -73,6 +75,7 @@ pub const Event = struct {
7375
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
7476
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
7577
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
78+
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
7679
};
7780
}
7881

src/browser/fetch/fetch.zig

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ pub const FetchContext = struct {
6464
var headers: Headers = .{};
6565

6666
// seems to be the highest priority
67-
const same_origin = try isSameOriginAsPage(self.url, self.page);
67+
const same_origin = try self.page.isSameOrigin(self.url);
6868

6969
// If the mode is "no-cors", we need to return this opaque/stripped Response.
7070
// https://developer.mozilla.org/en-US/docs/Web/API/Response/type
@@ -237,11 +237,6 @@ pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promi
237237
return resolver.promise();
238238
}
239239

240-
fn isSameOriginAsPage(url: []const u8, page: *const Page) !bool {
241-
const origin = try page.origin(page.call_arena);
242-
return std.mem.startsWith(u8, url, origin);
243-
}
244-
245240
const testing = @import("../../testing.zig");
246241
test "fetch: fetch" {
247242
try testing.htmlRunner("fetch/fetch.html");

src/browser/html/History.zig

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <francis@lightpanda.io>
4+
// Pierre Tachoire <pierre@lightpanda.io>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
const log = @import("../../log.zig");
21+
22+
const Env = @import("../env.zig").Env;
23+
const Page = @import("../page.zig").Page;
24+
25+
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
26+
const History = @This();
27+
28+
const HistoryEntry = struct {
29+
url: []const u8,
30+
// This is serialized as JSON because
31+
// History must survive a JsContext.
32+
state: ?[]u8,
33+
};
34+
35+
const ScrollRestorationMode = enum {
36+
auto,
37+
manual,
38+
39+
pub fn fromString(str: []const u8) ?ScrollRestorationMode {
40+
for (std.enums.values(ScrollRestorationMode)) |mode| {
41+
if (std.ascii.eqlIgnoreCase(str, @tagName(mode))) {
42+
return mode;
43+
}
44+
} else {
45+
return null;
46+
}
47+
}
48+
49+
pub fn toString(self: ScrollRestorationMode) []const u8 {
50+
return @tagName(self);
51+
}
52+
};
53+
54+
scroll_restoration: ScrollRestorationMode = .auto,
55+
stack: std.ArrayListUnmanaged(HistoryEntry) = .empty,
56+
current: ?usize = null,
57+
58+
pub fn get_length(self: *History) u32 {
59+
return @intCast(self.stack.items.len);
60+
}
61+
62+
pub fn get_scrollRestoration(self: *History) ScrollRestorationMode {
63+
return self.scroll_restoration;
64+
}
65+
66+
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
67+
self.scroll_restoration = ScrollRestorationMode.fromString(mode) orelse self.scroll_restoration;
68+
}
69+
70+
pub fn get_state(self: *History, page: *Page) !?Env.Value {
71+
if (self.current) |curr| {
72+
const entry = self.stack.items[curr];
73+
if (entry.state) |state| {
74+
const value = try Env.Value.fromJson(page.main_context, state);
75+
return value;
76+
} else {
77+
return null;
78+
}
79+
} else {
80+
return null;
81+
}
82+
}
83+
84+
pub fn pushNavigation(self: *History, _url: []const u8, page: *Page) !void {
85+
const arena = page.session.arena;
86+
const url = try arena.dupe(u8, _url);
87+
88+
const entry = HistoryEntry{ .state = null, .url = url };
89+
try self.stack.append(arena, entry);
90+
self.current = self.stack.items.len - 1;
91+
}
92+
93+
pub fn dispatchPopStateEvent(state: ?[]const u8, page: *Page) void {
94+
log.debug(.script_event, "dispatch popstate event", .{
95+
.type = "popstate",
96+
.source = "history",
97+
});
98+
History._dispatchPopStateEvent(state, page) catch |err| {
99+
log.err(.app, "dispatch popstate event error", .{
100+
.err = err,
101+
.type = "popstate",
102+
.source = "history",
103+
});
104+
};
105+
}
106+
107+
fn _dispatchPopStateEvent(state: ?[]const u8, page: *Page) !void {
108+
var evt = try PopStateEvent.constructor("popstate", .{ .state = state });
109+
110+
_ = try parser.eventTargetDispatchEvent(
111+
@as(*parser.EventTarget, @ptrCast(&page.window)),
112+
&evt.proto,
113+
);
114+
}
115+
116+
pub fn _pushState(self: *History, state: Env.JsObject, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
117+
const arena = page.session.arena;
118+
119+
const json = try state.toJson(arena);
120+
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
121+
const entry = HistoryEntry{ .state = json, .url = url };
122+
try self.stack.append(arena, entry);
123+
self.current = self.stack.items.len - 1;
124+
}
125+
126+
pub fn _replaceState(self: *History, state: Env.JsObject, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
127+
const arena = page.session.arena;
128+
129+
if (self.current) |curr| {
130+
const entry = &self.stack.items[curr];
131+
const json = try state.toJson(arena);
132+
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
133+
entry.* = HistoryEntry{ .state = json, .url = url };
134+
} else {
135+
try self._pushState(state, "", _url, page);
136+
}
137+
}
138+
139+
pub fn go(self: *History, delta: i32, page: *Page) !void {
140+
// 0 behaves the same as no argument, both reloading the page.
141+
// If this is getting called, there SHOULD be an entry, atleast from pushNavigation.
142+
const current = self.current.?;
143+
144+
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
145+
if (index_s < 0 or index_s > self.stack.items.len - 1) {
146+
return;
147+
}
148+
149+
const index = @as(usize, @intCast(index_s));
150+
const entry = self.stack.items[index];
151+
self.current = index;
152+
153+
if (try page.isSameOrigin(entry.url)) {
154+
History.dispatchPopStateEvent(entry.state, page);
155+
}
156+
157+
try page.navigateFromWebAPI(entry.url, .{ .reason = .history });
158+
}
159+
160+
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
161+
try self.go(_delta orelse 0, page);
162+
}
163+
164+
pub fn _back(self: *History, page: *Page) !void {
165+
try self.go(-1, page);
166+
}
167+
168+
pub fn _forward(self: *History, page: *Page) !void {
169+
try self.go(1, page);
170+
}
171+
172+
const parser = @import("../netsurf.zig");
173+
const Event = @import("../events/event.zig").Event;
174+
175+
pub const PopStateEvent = struct {
176+
pub const prototype = *Event;
177+
pub const union_make_copy = true;
178+
179+
pub const EventInit = struct {
180+
state: ?[]const u8 = null,
181+
};
182+
183+
proto: parser.Event,
184+
state: ?[]const u8,
185+
186+
pub fn constructor(event_type: []const u8, opts: ?EventInit) !PopStateEvent {
187+
const event = try parser.eventCreate();
188+
defer parser.eventDestroy(event);
189+
try parser.eventInit(event, event_type, .{});
190+
parser.eventSetInternalType(event, .pop_state);
191+
192+
const o = opts orelse EventInit{};
193+
194+
return .{
195+
.proto = event.*,
196+
.state = o.state,
197+
};
198+
}
199+
200+
// `hasUAVisualTransition` is not implemented. It isn't baseline so this is okay.
201+
202+
pub fn get_state(self: *const PopStateEvent, page: *Page) !?Env.Value {
203+
if (self.state) |state| {
204+
const value = try Env.Value.fromJson(page.main_context, state);
205+
return value;
206+
} else {
207+
return null;
208+
}
209+
}
210+
};
211+
212+
const testing = @import("../../testing.zig");
213+
test "Browser: HTML.History" {
214+
try testing.htmlRunner("html/history.html");
215+
}

src/browser/html/history.zig

Lines changed: 0 additions & 93 deletions
This file was deleted.

src/browser/html/html.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const HTMLElem = @import("elements.zig");
2121
const SVGElem = @import("svg_elements.zig");
2222
const Window = @import("window.zig").Window;
2323
const Navigator = @import("navigator.zig").Navigator;
24-
const History = @import("history.zig").History;
24+
const History = @import("History.zig");
2525
const Location = @import("location.zig").Location;
2626
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
2727

src/browser/html/window.zig

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const Env = @import("../env.zig").Env;
2424
const Page = @import("../page.zig").Page;
2525

2626
const Navigator = @import("navigator.zig").Navigator;
27-
const History = @import("history.zig").History;
27+
const History = @import("History.zig");
2828
const Location = @import("location.zig").Location;
2929
const Crypto = @import("../crypto/crypto.zig").Crypto;
3030
const Console = @import("../console/console.zig").Console;
@@ -54,7 +54,6 @@ pub const Window = struct {
5454

5555
document: *parser.DocumentHTML,
5656
target: []const u8 = "",
57-
history: History = .{},
5857
location: Location = .{},
5958
storage_shelf: ?*storage.Shelf = null,
6059

@@ -179,8 +178,8 @@ pub const Window = struct {
179178
return self.document;
180179
}
181180

182-
pub fn get_history(self: *Window) *History {
183-
return &self.history;
181+
pub fn get_history(_: *Window, page: *Page) *History {
182+
return &page.session.history;
184183
}
185184

186185
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.

src/browser/netsurf.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ pub const EventType = enum(u8) {
558558
xhr_event = 6,
559559
message_event = 7,
560560
keyboard_event = 8,
561+
pop_state = 9,
561562
};
562563

563564
pub const MutationEvent = c.dom_mutation_event;

0 commit comments

Comments
 (0)