From 186655e6149ed45131a64688af0def9038be2a88 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Sun, 12 Oct 2025 15:39:22 -0700 Subject: [PATCH] initial Navigation scaffolding --- src/browser/html/Navigation.zig | 221 ++++++++++++++++++++++++++++++++ src/browser/session.zig | 2 + 2 files changed, 223 insertions(+) create mode 100644 src/browser/html/Navigation.zig diff --git a/src/browser/html/Navigation.zig b/src/browser/html/Navigation.zig new file mode 100644 index 00000000..a0818828 --- /dev/null +++ b/src/browser/html/Navigation.zig @@ -0,0 +1,221 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const log = @import("../../log.zig"); +const URL = @import("../../url.zig").URL; + +const Js = @import("../js/js.zig"); +const Page = @import("../page.zig").Page; + +// https://developer.mozilla.org/en-US/docs/Web/API/Navigation +const Navigation = @This(); + +const EventTarget = @import("../dom/event_target.zig").EventTarget; +const EventHandler = @import("../events/event.zig").EventHandler; + +const parser = @import("../netsurf.zig"); + +const Interfaces = .{ + Navigation, + NavigationActivation, + NavigationHistoryEntry, +}; + +pub const prototype = *EventTarget; +base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain }, + +index: usize = 0, +entries: std.ArrayListUnmanaged(NavigationHistoryEntry) = .empty, +next_entry_id: usize = 0, +// TODO: key->index mapping + +// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry +const NavigationHistoryEntry = struct { + pub const prototype = *EventTarget; + base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain }, + + id: []const u8, + index: usize, + key: []const u8, + url: ?[]const u8, + same_document: bool, + state: ?[]const u8, + + pub fn get_id(self: *const NavigationHistoryEntry) []const u8 { + return self.id; + } + + pub fn get_index(self: *const NavigationHistoryEntry) usize { + return self.index; + } + + pub fn get_key(self: *const NavigationHistoryEntry) []const u8 { + return self.key; + } + + pub fn get_sameDocument(self: *const NavigationHistoryEntry) bool { + return self.same_document; + } + + pub fn get_url(self: *const NavigationHistoryEntry) ?[]const u8 { + return self.url; + } + + pub fn _getState(self: *const NavigationHistoryEntry, page: *Page) !?Js.Value { + if (self.state) |state| { + return try Js.Value.fromJson(page.main_context, state); + } else { + return null; + } + } +}; + +// https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation +const NavigationActivation = struct { + const NavigationActivationType = enum { + push, + reload, + replace, + traverse, + + pub fn toString(self: NavigationActivationType) []const u8 { + return @tagName(self); + } + }; + + entry: NavigationHistoryEntry, + from: ?NavigationHistoryEntry = null, + type: NavigationActivationType, + + pub fn get_entry(self: *const NavigationActivation) NavigationHistoryEntry { + return self.entry; + } + + pub fn get_from(self: *const NavigationActivation) ?NavigationHistoryEntry { + return self.from; + } + + pub fn get_navigationType(self: *const NavigationActivation) NavigationActivationType { + return self.type; + } +}; + +pub fn get_canGoBack(self: *const Navigation) bool { + return self.index > 0; +} + +pub fn get_canGoForward(self: *const Navigation) bool { + return self.entries.items.len > self.index + 1; +} + +pub fn get_currentEntry(_: *const Navigation) NavigationHistoryEntry { + // TODO + unreachable; +} + +const NavigationReturn = struct { + comitted: Js.Promise, + finished: Js.Promise, +}; + +pub fn _back(_: *const Navigation) !NavigationReturn { + unreachable; +} + +pub fn _entries(self: *const Navigation) []NavigationHistoryEntry { + return self.entries.items; +} + +pub fn _forward(_: *const Navigation) !NavigationReturn { + unreachable; +} + +const NavigateOptions = struct { + const NavigateOptionsHistory = enum { + auto, + push, + replace, + }; + + state: ?Js.Object = null, + info: ?Js.Object = null, + history: NavigateOptionsHistory = .auto, +}; + +pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn { + const arena = page.session.arena; + + const options = _opts orelse NavigateOptions{}; + const url = try arena.dupe(u8, _url); + + // TODO: handle push history NotSupportedError. + + const index = self.entries.items.len; + const id = self.next_entry_id; + self.next_entry_id += 1; + + const id_str = try std.fmt.allocPrint(arena, "{d}", .{id}); + + const state: ?[]const u8 = blk: { + if (options.state) |s| { + break :blk try s.toJson(arena); + } else { + break :blk null; + } + }; + + const entry = NavigationHistoryEntry{ + .id = id_str, + .index = index, + .same_document = false, + .url = url, + .key = id_str, + .state = state, + }; + + try self.entries.append(arena, entry); + + // https://github.com/WICG/navigation-api/issues/95 + // + // These will only settle on same-origin navigation (mostly intended for SPAs). + // It is fine (and expected) for these to not settle on cross-origin requests :) + const committed = try page.main_context.createPersistentPromiseResolver(.page); + const finished = try page.main_context.createPersistentPromiseResolver(.page); + + if (entry.same_document) { + page.url = try URL.parse(url, null); + try committed.resolve(void); + + // todo: Fire navigate event + // + + } else { + page.navigateFromWebAPI(url, .{ .reason = .navigation }); + } + + return .{ + .comitted = committed, + .finished = finished, + }; +} + +// const testing = @import("../../testing.zig"); +// test "Browser: Navigation" { +// try testing.htmlRunner("html/navigation.html"); +// } diff --git a/src/browser/session.zig b/src/browser/session.zig index a5651d20..3456f159 100644 --- a/src/browser/session.zig +++ b/src/browser/session.zig @@ -25,6 +25,7 @@ const Page = @import("page.zig").Page; const Browser = @import("browser.zig").Browser; const NavigateOpts = @import("page.zig").NavigateOpts; const History = @import("html/History.zig"); +const Navigation = @import("html/Navigation.zig"); const log = @import("../log.zig"); const parser = @import("netsurf.zig"); @@ -57,6 +58,7 @@ pub const Session = struct { // History is persistent across the "tab". // https://developer.mozilla.org/en-US/docs/Web/API/History history: History = .{}, + navigation: Navigation = .{}, page: ?Page = null,