mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
Compare commits
35 Commits
nikneym/ad
...
a4d290ba58
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4d290ba58 | ||
|
|
f69c0cc072 | ||
|
|
bfaf0fa00a | ||
|
|
02d9a670ff | ||
|
|
317916307f | ||
|
|
5ac40309cf | ||
|
|
882ed4d457 | ||
|
|
cb179794ae | ||
|
|
b5f0d017cc | ||
|
|
d579f21bf2 | ||
|
|
813e36f44e | ||
|
|
e0a912722b | ||
|
|
332b302285 | ||
|
|
b1e8268ce0 | ||
|
|
c52dce1c48 | ||
|
|
0b4a1b4a1b | ||
|
|
cc0c1bcf3a | ||
|
|
55746f1a1d | ||
|
|
7bb8581a95 | ||
|
|
521c0f8460 | ||
|
|
4bfe3b6fe1 | ||
|
|
b610aa1c0c | ||
|
|
73da04bea2 | ||
|
|
18c851e53f | ||
|
|
41f4533bc0 | ||
|
|
4db8a967b6 | ||
|
|
ff70f4e79f | ||
|
|
c9517aff7d | ||
|
|
3657a49a2c | ||
|
|
71e7aa5262 | ||
|
|
2e435f5d4e | ||
|
|
859b03c4a6 | ||
|
|
ee8786444f | ||
|
|
afac4fc37f | ||
|
|
de83521e08 |
@@ -384,6 +384,7 @@ pub fn getAsyncModule(self: *ScriptManager, url: [:0]const u8, cb: AsyncModule.C
|
||||
pub fn staticScriptsDone(self: *ScriptManager) void {
|
||||
std.debug.assert(self.static_scripts_done == false);
|
||||
self.static_scripts_done = true;
|
||||
self.evaluate();
|
||||
}
|
||||
|
||||
// try to evaluate completed scripts (in order). This is called whenever a script
|
||||
@@ -450,6 +451,12 @@ pub fn isDone(self: *const ScriptManager) bool {
|
||||
self.deferreds.first == null; // and there are no more <script defer src=> to wait for
|
||||
}
|
||||
|
||||
fn asyncScriptIsDone(self: *ScriptManager) void {
|
||||
if (self.isDone()) {
|
||||
self.page.documentIsComplete();
|
||||
}
|
||||
}
|
||||
|
||||
fn startCallback(transfer: *Http.Transfer) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
||||
script.startCallback(transfer) catch |err| {
|
||||
@@ -595,8 +602,10 @@ pub const PendingScript = struct {
|
||||
return;
|
||||
}
|
||||
// async script can be evaluated immediately
|
||||
defer self.deinit();
|
||||
self.script.eval(manager.page);
|
||||
self.deinit();
|
||||
// asyncScriptIsDone must be run after the pending script is deinit.
|
||||
manager.asyncScriptIsDone();
|
||||
}
|
||||
|
||||
fn errorCallback(self: *PendingScript, err: anyerror) void {
|
||||
|
||||
@@ -34,6 +34,7 @@ pub const Union = union(enum) {
|
||||
screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
|
||||
performance: *@import("performance.zig").Performance,
|
||||
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
|
||||
navigation: *@import("../navigation/Navigation.zig"),
|
||||
};
|
||||
|
||||
// EventTarget implementation
|
||||
@@ -82,6 +83,11 @@ pub const EventTarget = struct {
|
||||
.media_query_list => {
|
||||
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
.navigation => {
|
||||
const NavigationEventTarget = @import("../navigation/NavigationEventTarget.zig");
|
||||
const base: *NavigationEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
|
||||
return .{ .navigation = @fieldParentPtr("proto", base) };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !Veri
|
||||
const node_type = parser.nodeType(node);
|
||||
|
||||
// Verify that we can show this node type.
|
||||
// Per the DOM spec, what_to_show filters which nodes to return, but should
|
||||
// still traverse children. So we return .skip (not .reject) when the node
|
||||
// type doesn't match.
|
||||
if (!switch (node_type) {
|
||||
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
|
||||
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
|
||||
@@ -60,7 +63,7 @@ pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !Veri
|
||||
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
|
||||
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
|
||||
.text => what_to_show & NodeFilter._SHOW_TEXT != 0,
|
||||
}) return .reject;
|
||||
}) return .skip;
|
||||
|
||||
// Verify that we aren't filtering it out.
|
||||
if (filter) |f| {
|
||||
|
||||
@@ -74,10 +74,10 @@ pub const NodeIterator = struct {
|
||||
|
||||
return .{
|
||||
.root = node,
|
||||
.reference_node = node,
|
||||
.what_to_show = what_to_show,
|
||||
.filter = filter,
|
||||
.reference_node = node,
|
||||
.filter_func = filter_func,
|
||||
.what_to_show = what_to_show,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ pub const NodeIterator = struct {
|
||||
defer self.callbackEnd();
|
||||
|
||||
if (self.pointer_before_current) {
|
||||
self.pointer_before_current = false;
|
||||
// Unlike TreeWalker, NodeIterator starts at the first node
|
||||
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
||||
self.pointer_before_current = false;
|
||||
@@ -120,9 +121,21 @@ pub const NodeIterator = struct {
|
||||
|
||||
var current = self.reference_node;
|
||||
while (current != self.root) {
|
||||
if (try self.nextSibling(current)) |sibling| {
|
||||
self.reference_node = sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
// Try to get next sibling (including .skip/.reject nodes we need to descend into)
|
||||
if (try self.nextSiblingOrSkipReject(current)) |result| {
|
||||
if (result.should_descend) {
|
||||
// This is a .skip/.reject node - try to find acceptable children within it
|
||||
if (try self.firstChild(result.node)) |child| {
|
||||
self.reference_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
// No acceptable children, continue looking at this node's siblings
|
||||
current = result.node;
|
||||
continue;
|
||||
}
|
||||
// This is an .accept node - return it
|
||||
self.reference_node = result.node;
|
||||
return try Node.toInterface(result.node);
|
||||
}
|
||||
|
||||
current = (parser.nodeParentNode(current)) orelse break;
|
||||
@@ -254,6 +267,22 @@ pub const NodeIterator = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the next sibling that is either acceptable or should be descended into (skip/reject)
|
||||
fn nextSiblingOrSkipReject(self: *const NodeIterator, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
|
||||
var current = node;
|
||||
|
||||
while (true) {
|
||||
current = (parser.nodeNextSibling(current)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return .{ .node = current, .should_descend = false },
|
||||
.skip, .reject => return .{ .node = current, .should_descend = true },
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn callbackStart(self: *NodeIterator) !void {
|
||||
if (self.is_in_callback) {
|
||||
// this is the correct DOMExeption
|
||||
|
||||
@@ -144,6 +144,23 @@ pub const TreeWalker = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the next sibling that is either acceptable or should be descended into (skip)
|
||||
fn nextSiblingOrSkip(self: *const TreeWalker, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
|
||||
var current = node;
|
||||
|
||||
while (true) {
|
||||
current = (parser.nodeNextSibling(current)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return .{ .node = current, .should_descend = false },
|
||||
.skip => return .{ .node = current, .should_descend = true },
|
||||
.reject => continue,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn previousSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
||||
var current = node;
|
||||
|
||||
@@ -193,19 +210,37 @@ pub const TreeWalker = struct {
|
||||
}
|
||||
|
||||
pub fn _nextNode(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.firstChild(self.current_node)) |child| {
|
||||
var current = self.current_node;
|
||||
|
||||
// First, try to go to first child of current node
|
||||
if (try self.firstChild(current)) |child| {
|
||||
self.current_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
var current = self.current_node;
|
||||
// No acceptable children, move to next node in tree
|
||||
while (current != self.root) {
|
||||
if (try self.nextSibling(current)) |sibling| {
|
||||
self.current_node = sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
const result = try self.nextSiblingOrSkip(current) orelse {
|
||||
// No next sibling, go up to parent and continue
|
||||
// or, if there is no parent, we're done
|
||||
current = (parser.nodeParentNode(current)) orelse break;
|
||||
continue;
|
||||
};
|
||||
|
||||
|
||||
if (!result.should_descend) {
|
||||
// This is an .accept node - return it
|
||||
self.current_node = result.node;
|
||||
return try Node.toInterface(result.node);
|
||||
}
|
||||
|
||||
current = (parser.nodeParentNode(current)) orelse break;
|
||||
// This is a .skip node - try to find acceptable children within it
|
||||
if (try self.firstChild(result.node)) |child| {
|
||||
self.current_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
// No acceptable children, continue looking at this node's siblings
|
||||
current = result.node;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -236,10 +236,10 @@ fn isVoid(elem: *parser.Element) !bool {
|
||||
};
|
||||
}
|
||||
|
||||
fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
|
||||
fn writeEscapedTextNode(writer: *std.Io.Writer, value: []const u8) !void {
|
||||
var v = value;
|
||||
while (v.len > 0) {
|
||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
|
||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', 194 }) orelse {
|
||||
return writer.writeAll(v);
|
||||
};
|
||||
try writer.writeAll(v[0..index]);
|
||||
@@ -247,13 +247,22 @@ fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
|
||||
'&' => try writer.writeAll("&"),
|
||||
'<' => try writer.writeAll("<"),
|
||||
'>' => try writer.writeAll(">"),
|
||||
194 => {
|
||||
// non breaking space
|
||||
if (v.len > index + 1 and v[index + 1] == 160) {
|
||||
try writer.writeAll(" ");
|
||||
v = v[index + 2 ..];
|
||||
continue;
|
||||
}
|
||||
try writer.writeByte(194);
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
v = v[index + 1 ..];
|
||||
}
|
||||
}
|
||||
|
||||
fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
|
||||
fn writeEscapedAttributeValue(writer: *std.Io.Writer, value: []const u8) !void {
|
||||
var v = value;
|
||||
while (v.len > 0) {
|
||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
@@ -37,6 +38,7 @@ const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
|
||||
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
|
||||
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
|
||||
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
|
||||
const NavigationCurrentEntryChangeEvent = @import("../navigation/navigation.zig").NavigationCurrentEntryChangeEvent;
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = .{
|
||||
@@ -48,6 +50,7 @@ pub const Interfaces = .{
|
||||
ErrorEvent,
|
||||
MessageEvent,
|
||||
PopStateEvent,
|
||||
NavigationCurrentEntryChangeEvent,
|
||||
};
|
||||
|
||||
pub const Union = generate.Union(Interfaces);
|
||||
@@ -76,6 +79,9 @@ pub const Event = struct {
|
||||
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
|
||||
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
|
||||
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
|
||||
.navigation_current_entry_change_event => .{
|
||||
.NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -223,8 +229,6 @@ pub const EventHandler = struct {
|
||||
node: parser.EventNode,
|
||||
listener: *parser.EventListener,
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
pub const Listener = union(enum) {
|
||||
function: js.Function,
|
||||
object: js.Object,
|
||||
@@ -396,6 +400,40 @@ const SignalCallback = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub fn DirectEventHandler(
|
||||
comptime TargetT: type,
|
||||
target: *TargetT,
|
||||
event_type: []const u8,
|
||||
maybe_listener: ?EventHandler.Listener,
|
||||
cb: *?js.Function,
|
||||
page_arena: std.mem.Allocator,
|
||||
) !void {
|
||||
const event_target = parser.toEventTarget(TargetT, target);
|
||||
|
||||
// Check if we have a listener set.
|
||||
if (cb.*) |callback| {
|
||||
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id);
|
||||
std.debug.assert(listener != null);
|
||||
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
|
||||
}
|
||||
|
||||
if (maybe_listener) |listener| {
|
||||
switch (listener) {
|
||||
// If an object is given as listener, do nothing.
|
||||
.object => {},
|
||||
.function => |callback| {
|
||||
_ = try EventHandler.register(page_arena, event_target, event_type, listener, null) orelse unreachable;
|
||||
cb.* = callback;
|
||||
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Just unset the listener.
|
||||
cb.* = null;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Event" {
|
||||
try testing.htmlRunner("events/event.html");
|
||||
|
||||
@@ -21,140 +21,79 @@ const log = @import("../../log.zig");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Window = @import("window.zig").Window;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
||||
const History = @This();
|
||||
|
||||
const HistoryEntry = struct {
|
||||
url: []const u8,
|
||||
// This is serialized as JSON because
|
||||
// History must survive a JsContext.
|
||||
state: ?[]u8,
|
||||
};
|
||||
|
||||
const ScrollRestorationMode = enum {
|
||||
pub const ENUM_JS_USE_TAG = true;
|
||||
|
||||
auto,
|
||||
manual,
|
||||
|
||||
pub fn fromString(str: []const u8) ?ScrollRestorationMode {
|
||||
for (std.enums.values(ScrollRestorationMode)) |mode| {
|
||||
if (std.ascii.eqlIgnoreCase(str, @tagName(mode))) {
|
||||
return mode;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toString(self: ScrollRestorationMode) []const u8 {
|
||||
return @tagName(self);
|
||||
}
|
||||
};
|
||||
|
||||
scroll_restoration: ScrollRestorationMode = .auto,
|
||||
stack: std.ArrayListUnmanaged(HistoryEntry) = .empty,
|
||||
current: ?usize = null,
|
||||
|
||||
pub fn get_length(self: *History) u32 {
|
||||
return @intCast(self.stack.items.len);
|
||||
pub fn get_length(_: *History, page: *Page) u32 {
|
||||
return @intCast(page.session.navigation.entries.items.len);
|
||||
}
|
||||
|
||||
pub fn get_scrollRestoration(self: *History) ScrollRestorationMode {
|
||||
return self.scroll_restoration;
|
||||
}
|
||||
|
||||
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
|
||||
self.scroll_restoration = ScrollRestorationMode.fromString(mode) orelse self.scroll_restoration;
|
||||
pub fn set_scrollRestoration(self: *History, mode: ScrollRestorationMode) void {
|
||||
self.scroll_restoration = mode;
|
||||
}
|
||||
|
||||
pub fn get_state(self: *History, page: *Page) !?js.Value {
|
||||
if (self.current) |curr| {
|
||||
const entry = self.stack.items[curr];
|
||||
if (entry.state) |state| {
|
||||
const value = try js.Value.fromJson(page.js, state);
|
||||
return value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
pub fn get_state(_: *History, page: *Page) !?js.Value {
|
||||
if (page.session.navigation.currentEntry().state) |state| {
|
||||
const value = try js.Value.fromJson(page.js, state);
|
||||
return value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pushNavigation(self: *History, _url: []const u8, page: *Page) !void {
|
||||
pub fn _pushState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
||||
const arena = page.session.arena;
|
||||
const url = try arena.dupe(u8, _url);
|
||||
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
||||
|
||||
const entry = HistoryEntry{ .state = null, .url = url };
|
||||
try self.stack.append(arena, entry);
|
||||
self.current = self.stack.items.len - 1;
|
||||
const json = state.toJson(arena) catch return error.DataClone;
|
||||
_ = try page.session.navigation.pushEntry(url, json, page, true);
|
||||
}
|
||||
|
||||
pub fn dispatchPopStateEvent(state: ?[]const u8, page: *Page) void {
|
||||
log.debug(.script_event, "dispatch popstate event", .{
|
||||
.type = "popstate",
|
||||
.source = "history",
|
||||
});
|
||||
History._dispatchPopStateEvent(state, page) catch |err| {
|
||||
log.err(.app, "dispatch popstate event error", .{
|
||||
.err = err,
|
||||
.type = "popstate",
|
||||
.source = "history",
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
fn _dispatchPopStateEvent(state: ?[]const u8, page: *Page) !void {
|
||||
var evt = try PopStateEvent.constructor("popstate", .{ .state = state });
|
||||
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
@as(*parser.EventTarget, @ptrCast(&page.window)),
|
||||
&evt.proto,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn _pushState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
||||
pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
||||
const arena = page.session.arena;
|
||||
|
||||
const entry = page.session.navigation.currentEntry();
|
||||
const json = try state.toJson(arena);
|
||||
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
||||
const entry = HistoryEntry{ .state = json, .url = url };
|
||||
try self.stack.append(arena, entry);
|
||||
self.current = self.stack.items.len - 1;
|
||||
|
||||
entry.state = json;
|
||||
entry.url = url;
|
||||
}
|
||||
|
||||
pub fn _replaceState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
||||
const arena = page.session.arena;
|
||||
|
||||
if (self.current) |curr| {
|
||||
const entry = &self.stack.items[curr];
|
||||
const json = try state.toJson(arena);
|
||||
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
||||
entry.* = HistoryEntry{ .state = json, .url = url };
|
||||
} else {
|
||||
try self._pushState(state, "", _url, page);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go(self: *History, delta: i32, page: *Page) !void {
|
||||
pub fn go(_: *const History, delta: i32, page: *Page) !void {
|
||||
// 0 behaves the same as no argument, both reloading the page.
|
||||
// If this is getting called, there SHOULD be an entry, atleast from pushNavigation.
|
||||
const current = self.current.?;
|
||||
|
||||
const current = page.session.navigation.index;
|
||||
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
|
||||
if (index_s < 0 or index_s > self.stack.items.len - 1) {
|
||||
if (index_s < 0 or index_s > page.session.navigation.entries.items.len - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = @as(usize, @intCast(index_s));
|
||||
const entry = self.stack.items[index];
|
||||
self.current = index;
|
||||
const entry = page.session.navigation.entries.items[index];
|
||||
|
||||
if (try page.isSameOrigin(entry.url)) {
|
||||
History.dispatchPopStateEvent(entry.state, page);
|
||||
if (entry.url) |url| {
|
||||
if (try page.isSameOrigin(url)) {
|
||||
PopStateEvent.dispatch(entry.state, page);
|
||||
}
|
||||
}
|
||||
|
||||
try page.navigateFromWebAPI(entry.url, .{ .reason = .history });
|
||||
_ = try page.session.navigation.navigate(entry.url, .{ .traverse = index }, page);
|
||||
}
|
||||
|
||||
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
|
||||
@@ -207,9 +146,38 @@ pub const PopStateEvent = struct {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch(state: ?[]const u8, page: *Page) void {
|
||||
log.debug(.script_event, "dispatch popstate event", .{
|
||||
.type = "popstate",
|
||||
.source = "history",
|
||||
});
|
||||
|
||||
var evt = PopStateEvent.constructor("popstate", .{ .state = state }) catch |err| {
|
||||
log.err(.app, "event constructor error", .{
|
||||
.err = err,
|
||||
.type = "popstate",
|
||||
.source = "history",
|
||||
});
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
_ = parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(Window, &page.window),
|
||||
&evt.proto,
|
||||
) catch |err| {
|
||||
log.err(.app, "dispatch popstate event error", .{
|
||||
.err = err,
|
||||
.type = "popstate",
|
||||
.source = "history",
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: HTML.History" {
|
||||
try testing.htmlRunner("html/history.html");
|
||||
try testing.htmlRunner("html/history/history.html");
|
||||
try testing.htmlRunner("html/history/history2.html");
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
|
||||
}
|
||||
|
||||
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {
|
||||
|
||||
@@ -38,7 +38,7 @@ pub const Location = struct {
|
||||
}
|
||||
|
||||
pub fn set_href(_: *const Location, href: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(href, .{ .reason = .script });
|
||||
return page.navigateFromWebAPI(href, .{ .reason = .script }, .{ .push = null });
|
||||
}
|
||||
|
||||
pub fn get_protocol(self: *Location) []const u8 {
|
||||
@@ -74,15 +74,15 @@ pub const Location = struct {
|
||||
}
|
||||
|
||||
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
|
||||
}
|
||||
|
||||
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script }, .replace);
|
||||
}
|
||||
|
||||
pub fn _reload(_: *const Location, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script });
|
||||
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script }, .reload);
|
||||
}
|
||||
|
||||
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
|
||||
|
||||
@@ -25,6 +25,7 @@ const Page = @import("../page.zig").Page;
|
||||
|
||||
const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("History.zig");
|
||||
const Navigation = @import("../navigation/Navigation.zig");
|
||||
const Location = @import("location.zig").Location;
|
||||
const Crypto = @import("../crypto/crypto.zig").Crypto;
|
||||
const Console = @import("../console/console.zig").Console;
|
||||
@@ -42,6 +43,8 @@ const fetchFn = @import("../fetch/fetch.zig").fetch;
|
||||
|
||||
const storage = @import("../storage/storage.zig");
|
||||
|
||||
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-window-extensions
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
|
||||
pub const Window = struct {
|
||||
@@ -68,6 +71,7 @@ pub const Window = struct {
|
||||
scroll_x: u32 = 0,
|
||||
scroll_y: u32 = 0,
|
||||
onload_callback: ?js.Function = null,
|
||||
onpopstate_callback: ?js.Function = null,
|
||||
|
||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
|
||||
var fbs = std.io.fixedBufferStream("");
|
||||
@@ -109,31 +113,17 @@ pub const Window = struct {
|
||||
|
||||
/// Sets `onload_callback`.
|
||||
pub fn set_onload(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
|
||||
const event_target = parser.toEventTarget(Window, self);
|
||||
const event_type = "load";
|
||||
try DirectEventHandler(Window, self, "load", maybe_listener, &self.onload_callback, page.arena);
|
||||
}
|
||||
|
||||
// Check if we have a listener set.
|
||||
if (self.onload_callback) |callback| {
|
||||
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id);
|
||||
std.debug.assert(listener != null);
|
||||
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
|
||||
}
|
||||
/// Returns `onpopstate_callback`.
|
||||
pub fn get_onpopstate(self: *const Window) ?js.Function {
|
||||
return self.onpopstate_callback;
|
||||
}
|
||||
|
||||
if (maybe_listener) |listener| {
|
||||
switch (listener) {
|
||||
// If an object is given as listener, do nothing.
|
||||
.object => {},
|
||||
.function => |callback| {
|
||||
_ = try EventHandler.register(page.arena, event_target, event_type, listener, null) orelse unreachable;
|
||||
self.onload_callback = callback;
|
||||
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Just unset the listener.
|
||||
self.onload_callback = null;
|
||||
/// Sets `onpopstate_callback`.
|
||||
pub fn set_onpopstate(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
|
||||
try DirectEventHandler(Window, self, "popstate", maybe_listener, &self.onpopstate_callback, page.arena);
|
||||
}
|
||||
|
||||
pub fn get_location(self: *Window) *Location {
|
||||
@@ -141,7 +131,7 @@ pub const Window = struct {
|
||||
}
|
||||
|
||||
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
|
||||
}
|
||||
|
||||
// frames return the window itself, but accessing it via a pseudo
|
||||
@@ -189,6 +179,10 @@ pub const Window = struct {
|
||||
return &page.session.history;
|
||||
}
|
||||
|
||||
pub fn get_navigation(_: *Window, page: *Page) *Navigation {
|
||||
return &page.session.navigation;
|
||||
}
|
||||
|
||||
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
|
||||
pub fn get_innerHeight(_: *Window, page: *Page) u32 {
|
||||
// We do not have scrollbars or padding so this is the same as Element.clientHeight
|
||||
|
||||
@@ -262,7 +262,7 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
|
||||
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
|
||||
gop.key_ptr.* = owned_specifier;
|
||||
gop.value_ptr.* = .{};
|
||||
try self.script_manager.?.getModule(owned_specifier, src);
|
||||
try self.script_manager.?.getModule(owned_specifier, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,7 +271,18 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
|
||||
return error.ModuleInstantiationError;
|
||||
}
|
||||
|
||||
const evaluated = try m.evaluate(v8_context);
|
||||
const evaluated = m.evaluate(v8_context) catch {
|
||||
std.debug.assert(m.getStatus() == .kErrored);
|
||||
|
||||
// Some module-loading errors aren't handled by TryCatch. We need to
|
||||
// get the error from the module itself.
|
||||
log.warn(.js, "evaluate module", .{
|
||||
.specifier = owned_url,
|
||||
.message = self.valueToString(m.getException(), .{}) catch "???",
|
||||
});
|
||||
return error.EvaluationError;
|
||||
};
|
||||
|
||||
// https://v8.github.io/api/head/classv8_1_1Module.html#a1f1758265a4082595757c3251bb40e0f
|
||||
// Must be a promise that gets returned here.
|
||||
std.debug.assert(evaluated.isPromise());
|
||||
@@ -739,9 +750,16 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
|
||||
unreachable;
|
||||
},
|
||||
.@"enum" => |e| {
|
||||
switch (@typeInfo(e.tag_type)) {
|
||||
.int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_value, self.v8_context)),
|
||||
else => @compileError(named_function.full_name ++ " has an unsupported enum parameter type: " ++ @typeName(T)),
|
||||
if (@hasDecl(T, "ENUM_JS_USE_TAG")) {
|
||||
const str = try self.jsValueToZig(named_function, []const u8, js_value);
|
||||
return std.meta.stringToEnum(T, str) orelse return error.InvalidEnumValue;
|
||||
} else {
|
||||
switch (@typeInfo(e.tag_type)) {
|
||||
.int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_value, self.v8_context)),
|
||||
else => {
|
||||
@compileError(named_function.full_name ++ " has an unsupported enum parameter type: " ++ @typeName(T));
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
@@ -1171,7 +1189,7 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
|
||||
};
|
||||
|
||||
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
|
||||
self.call_arena,
|
||||
self.arena, // might need to survive until the module is loaded
|
||||
specifier,
|
||||
referrer_path,
|
||||
);
|
||||
@@ -1206,13 +1224,21 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
|
||||
defer try_catch.deinit();
|
||||
|
||||
const entry = self.module(true, fetch_result.src(), normalized_specifier, true) catch |err| {
|
||||
log.warn(.js, "compile resolved module", .{
|
||||
.specifier = specifier,
|
||||
.stack = try_catch.stack(self.call_arena) catch null,
|
||||
.src = try_catch.sourceLine(self.call_arena) catch "err",
|
||||
.line = try_catch.sourceLineNumber() orelse 0,
|
||||
.exception = (try_catch.exception(self.call_arena) catch @errorName(err)) orelse @errorName(err),
|
||||
});
|
||||
switch (err) {
|
||||
error.EvaluationError => {
|
||||
// This is a sentinel value telling us that the error was already
|
||||
// logged. Some module-loading errors aren't captured by Try/Catch.
|
||||
// We need to handle those errors differently, where the module
|
||||
// exists.
|
||||
},
|
||||
else => log.warn(.js, "compile resolved module", .{
|
||||
.specifier = normalized_specifier,
|
||||
.stack = try_catch.stack(self.call_arena) catch null,
|
||||
.src = try_catch.sourceLine(self.call_arena) catch "err",
|
||||
.line = try_catch.sourceLineNumber() orelse 0,
|
||||
.exception = (try_catch.exception(self.call_arena) catch @errorName(err)) orelse @errorName(err),
|
||||
}),
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// entry.module is always set when returning from self.module()
|
||||
|
||||
@@ -378,8 +378,13 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
|
||||
.@"enum" => {
|
||||
const T = @TypeOf(value);
|
||||
if (@hasDecl(T, "toString")) {
|
||||
// This should be deprecated in favor of the ENUM_JS_USE_TAG.
|
||||
return simpleZigValueToJs(isolate, value.toString(), fail);
|
||||
}
|
||||
|
||||
if (@hasDecl(T, "ENUM_JS_USE_TAG")) {
|
||||
return simpleZigValueToJs(isolate, @tagName(value), fail);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const Interfaces = generate.Tuple(.{
|
||||
@import("../storage/storage.zig").Interfaces,
|
||||
@import("../url/url.zig").Interfaces,
|
||||
@import("../xhr/xhr.zig").Interfaces,
|
||||
@import("../navigation/navigation.zig").Interfaces,
|
||||
@import("../xhr/form_data.zig").Interfaces,
|
||||
@import("../xhr/File.zig"),
|
||||
@import("../xmlserializer/xmlserializer.zig").Interfaces,
|
||||
|
||||
292
src/browser/navigation/Navigation.zig
Normal file
292
src/browser/navigation/Navigation.zig
Normal file
@@ -0,0 +1,292 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
|
||||
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Navigation
|
||||
const Navigation = @This();
|
||||
|
||||
const NavigationKind = @import("navigation.zig").NavigationKind;
|
||||
const NavigationHistoryEntry = @import("navigation.zig").NavigationHistoryEntry;
|
||||
const NavigationTransition = @import("navigation.zig").NavigationTransition;
|
||||
const NavigationEventTarget = @import("NavigationEventTarget.zig");
|
||||
|
||||
const NavigationCurrentEntryChangeEvent = @import("navigation.zig").NavigationCurrentEntryChangeEvent;
|
||||
|
||||
pub const prototype = *NavigationEventTarget;
|
||||
proto: NavigationEventTarget = NavigationEventTarget{},
|
||||
|
||||
index: usize = 0,
|
||||
// Need to be stable pointers, because Events can reference entries.
|
||||
entries: std.ArrayListUnmanaged(*NavigationHistoryEntry) = .empty,
|
||||
next_entry_id: usize = 0,
|
||||
|
||||
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 currentEntry(self: *Navigation) *NavigationHistoryEntry {
|
||||
return self.entries.items[self.index];
|
||||
}
|
||||
|
||||
pub fn get_currentEntry(self: *Navigation) *NavigationHistoryEntry {
|
||||
return self.currentEntry();
|
||||
}
|
||||
|
||||
pub fn get_transition(_: *const Navigation) ?NavigationTransition {
|
||||
// For now, all transitions are just considered complete.
|
||||
return null;
|
||||
}
|
||||
|
||||
const NavigationReturn = struct {
|
||||
committed: js.Promise,
|
||||
finished: js.Promise,
|
||||
};
|
||||
|
||||
pub fn _back(self: *Navigation, page: *Page) !NavigationReturn {
|
||||
if (!self.get_canGoBack()) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
|
||||
const new_index = self.index - 1;
|
||||
const next_entry = self.entries.items[new_index];
|
||||
self.index = new_index;
|
||||
|
||||
return self.navigate(next_entry.url, .{ .traverse = new_index }, page);
|
||||
}
|
||||
|
||||
pub fn _entries(self: *const Navigation) []*NavigationHistoryEntry {
|
||||
return self.entries.items;
|
||||
}
|
||||
|
||||
pub fn _forward(self: *Navigation, page: *Page) !NavigationReturn {
|
||||
if (!self.get_canGoForward()) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
|
||||
const new_index = self.index + 1;
|
||||
const next_entry = self.entries.items[new_index];
|
||||
self.index = new_index;
|
||||
|
||||
return self.navigate(next_entry.url, .{ .traverse = new_index }, page);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes an entry into the Navigation stack WITHOUT actually navigating to it.
|
||||
/// For that, use `navigate`.
|
||||
pub fn pushEntry(self: *Navigation, _url: ?[]const u8, state: ?[]const u8, page: *Page, dispatch: bool) !*NavigationHistoryEntry {
|
||||
const arena = page.session.arena;
|
||||
|
||||
const url = if (_url) |u| try arena.dupe(u8, u) else null;
|
||||
|
||||
// truncates our history here.
|
||||
if (self.entries.items.len > self.index + 1) {
|
||||
self.entries.shrinkRetainingCapacity(self.index + 1);
|
||||
}
|
||||
|
||||
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 entry = try arena.create(NavigationHistoryEntry);
|
||||
entry.* = NavigationHistoryEntry{
|
||||
.id = id_str,
|
||||
.key = id_str,
|
||||
.url = url,
|
||||
.state = state,
|
||||
};
|
||||
|
||||
// we don't always have a current entry...
|
||||
const previous = if (self.entries.items.len > 0) self.currentEntry() else null;
|
||||
try self.entries.append(arena, entry);
|
||||
if (previous) |prev| {
|
||||
if (dispatch) {
|
||||
NavigationCurrentEntryChangeEvent.dispatch(self, prev, .push);
|
||||
}
|
||||
}
|
||||
|
||||
self.index = index;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
const NavigateOptions = struct {
|
||||
const NavigateOptionsHistory = enum {
|
||||
pub const ENUM_JS_USE_TAG = true;
|
||||
|
||||
auto,
|
||||
push,
|
||||
replace,
|
||||
};
|
||||
|
||||
state: ?js.Object = null,
|
||||
info: ?js.Object = null,
|
||||
history: NavigateOptionsHistory = .auto,
|
||||
};
|
||||
|
||||
pub fn navigate(
|
||||
self: *Navigation,
|
||||
_url: ?[]const u8,
|
||||
kind: NavigationKind,
|
||||
page: *Page,
|
||||
) !NavigationReturn {
|
||||
const arena = page.session.arena;
|
||||
const url = _url orelse return error.MissingURL;
|
||||
|
||||
// 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.js.createPromiseResolver(.page);
|
||||
const finished = try page.js.createPromiseResolver(.page);
|
||||
|
||||
const new_url = try URL.parse(url, null);
|
||||
const is_same_document = try page.url.eqlDocument(&new_url, arena);
|
||||
|
||||
switch (kind) {
|
||||
.push => |state| {
|
||||
if (is_same_document) {
|
||||
page.url = new_url;
|
||||
|
||||
try committed.resolve({});
|
||||
// todo: Fire navigate event
|
||||
try finished.resolve({});
|
||||
|
||||
_ = try self.pushEntry(url, state, page, true);
|
||||
} else {
|
||||
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
|
||||
}
|
||||
},
|
||||
.traverse => |index| {
|
||||
self.index = index;
|
||||
|
||||
if (is_same_document) {
|
||||
page.url = new_url;
|
||||
|
||||
try committed.resolve({});
|
||||
// todo: Fire navigate event
|
||||
try finished.resolve({});
|
||||
} else {
|
||||
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
|
||||
}
|
||||
},
|
||||
.reload => {
|
||||
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
return .{
|
||||
.committed = committed.promise(),
|
||||
.finished = finished.promise(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn {
|
||||
const opts = _opts orelse NavigateOptions{};
|
||||
const json = if (opts.state) |state| state.toJson(page.session.arena) catch return error.DataClone else null;
|
||||
return try self.navigate(_url, .{ .push = json }, page);
|
||||
}
|
||||
|
||||
pub const ReloadOptions = struct {
|
||||
state: ?js.Object = null,
|
||||
info: ?js.Object = null,
|
||||
};
|
||||
|
||||
pub fn _reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !NavigationReturn {
|
||||
const arena = page.session.arena;
|
||||
|
||||
const opts = _opts orelse ReloadOptions{};
|
||||
const entry = self.currentEntry();
|
||||
if (opts.state) |state| {
|
||||
const previous = entry;
|
||||
entry.state = state.toJson(arena) catch return error.DataClone;
|
||||
NavigationCurrentEntryChangeEvent.dispatch(self, previous, .reload);
|
||||
}
|
||||
|
||||
return self.navigate(entry.url, .reload, page);
|
||||
}
|
||||
|
||||
pub const TraverseToOptions = struct {
|
||||
info: ?js.Object = null,
|
||||
};
|
||||
|
||||
pub fn _traverseTo(self: *Navigation, key: []const u8, _: ?TraverseToOptions, page: *Page) !NavigationReturn {
|
||||
// const opts = _opts orelse TraverseToOptions{};
|
||||
|
||||
for (self.entries.items, 0..) |entry, i| {
|
||||
if (std.mem.eql(u8, key, entry.key)) {
|
||||
return try self.navigate(entry.url, .{ .traverse = i }, page);
|
||||
}
|
||||
}
|
||||
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
|
||||
pub const UpdateCurrentEntryOptions = struct {
|
||||
state: js.Object,
|
||||
};
|
||||
|
||||
pub fn _updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions, page: *Page) !void {
|
||||
const arena = page.session.arena;
|
||||
|
||||
const previous = self.currentEntry();
|
||||
self.currentEntry().state = options.state.toJson(arena) catch return error.DataClone;
|
||||
NavigationCurrentEntryChangeEvent.dispatch(self, previous, null);
|
||||
}
|
||||
56
src/browser/navigation/NavigationEventTarget.zig
Normal file
56
src/browser/navigation/NavigationEventTarget.zig
Normal file
@@ -0,0 +1,56 @@
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
pub const NavigationEventTarget = @This();
|
||||
|
||||
pub const prototype = *EventTarget;
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .navigation },
|
||||
|
||||
oncurrententrychange_cbk: ?js.Function = null,
|
||||
|
||||
fn register(
|
||||
self: *NavigationEventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
typ: []const u8,
|
||||
listener: EventHandler.Listener,
|
||||
) !?js.Function {
|
||||
const target = @as(*parser.EventTarget, @ptrCast(self));
|
||||
|
||||
// The only time this can return null if the listener is already
|
||||
// registered. But before calling `register`, all of our functions
|
||||
// remove any existing listener, so it should be impossible to get null
|
||||
// from this function call.
|
||||
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
|
||||
return eh.callback;
|
||||
}
|
||||
|
||||
fn unregister(self: *NavigationEventTarget, typ: []const u8, cbk_id: usize) !void {
|
||||
const et = @as(*parser.EventTarget, @ptrCast(self));
|
||||
// check if event target has already this listener
|
||||
const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id);
|
||||
if (lst == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove listener
|
||||
try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
|
||||
}
|
||||
|
||||
pub fn get_oncurrententrychange(self: *NavigationEventTarget) ?js.Function {
|
||||
return self.oncurrententrychange_cbk;
|
||||
}
|
||||
|
||||
pub fn set_oncurrententrychange(self: *NavigationEventTarget, listener: ?EventHandler.Listener, page: *Page) !void {
|
||||
if (self.oncurrententrychange_cbk) |cbk| try self.unregister("currententrychange", cbk.id);
|
||||
if (listener) |listen| {
|
||||
self.oncurrententrychange_cbk = try self.register(page.arena, "currententrychange", listen);
|
||||
}
|
||||
}
|
||||
215
src/browser/navigation/navigation.zig
Normal file
215
src/browser/navigation/navigation.zig
Normal file
@@ -0,0 +1,215 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
|
||||
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Navigation = @import("Navigation.zig");
|
||||
const NavigationEventTarget = @import("NavigationEventTarget.zig");
|
||||
|
||||
pub const Interfaces = .{
|
||||
Navigation,
|
||||
NavigationEventTarget,
|
||||
NavigationActivation,
|
||||
NavigationTransition,
|
||||
NavigationHistoryEntry,
|
||||
};
|
||||
|
||||
pub const NavigationType = enum {
|
||||
pub const ENUM_JS_USE_TAG = true;
|
||||
|
||||
push,
|
||||
replace,
|
||||
traverse,
|
||||
reload,
|
||||
};
|
||||
|
||||
pub const NavigationKind = union(NavigationType) {
|
||||
push: ?[]const u8,
|
||||
replace,
|
||||
traverse: usize,
|
||||
reload,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry
|
||||
pub const NavigationHistoryEntry = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain },
|
||||
|
||||
id: []const u8,
|
||||
key: []const u8,
|
||||
url: ?[]const u8,
|
||||
state: ?[]const u8,
|
||||
|
||||
pub fn get_id(self: *const NavigationHistoryEntry) []const u8 {
|
||||
return self.id;
|
||||
}
|
||||
|
||||
pub fn get_index(self: *const NavigationHistoryEntry, page: *Page) i32 {
|
||||
const navigation = page.session.navigation;
|
||||
for (navigation.entries.items, 0..) |entry, i| {
|
||||
if (std.mem.eql(u8, entry.id, self.id)) {
|
||||
return @intCast(i);
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
pub fn get_key(self: *const NavigationHistoryEntry) []const u8 {
|
||||
return self.key;
|
||||
}
|
||||
|
||||
pub fn get_sameDocument(self: *const NavigationHistoryEntry, page: *Page) !bool {
|
||||
const _url = self.url orelse return false;
|
||||
const url = try URL.parse(_url, null);
|
||||
return page.url.eqlDocument(&url, page.arena);
|
||||
}
|
||||
|
||||
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.js, state);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation
|
||||
pub const NavigationActivation = struct {
|
||||
const NavigationActivationType = enum {
|
||||
pub const ENUM_JS_USE_TAG = true;
|
||||
|
||||
push,
|
||||
reload,
|
||||
replace,
|
||||
traverse,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationTransition
|
||||
pub const NavigationTransition = struct {
|
||||
finished: js.Promise,
|
||||
from: NavigationHistoryEntry,
|
||||
navigation_type: NavigationActivation.NavigationActivationType,
|
||||
};
|
||||
|
||||
const Event = @import("../events/event.zig").Event;
|
||||
|
||||
pub const NavigationCurrentEntryChangeEvent = struct {
|
||||
pub const prototype = *Event;
|
||||
pub const union_make_copy = true;
|
||||
|
||||
pub const EventInit = struct {
|
||||
from: *NavigationHistoryEntry,
|
||||
navigationType: ?NavigationType = null,
|
||||
};
|
||||
|
||||
proto: parser.Event,
|
||||
from: *NavigationHistoryEntry,
|
||||
navigation_type: ?NavigationType,
|
||||
|
||||
pub fn constructor(event_type: []const u8, opts: EventInit) !NavigationCurrentEntryChangeEvent {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
|
||||
try parser.eventInit(event, event_type, .{});
|
||||
parser.eventSetInternalType(event, .navigation_current_entry_change_event);
|
||||
|
||||
return .{
|
||||
.proto = event.*,
|
||||
.from = opts.from,
|
||||
.navigation_type = opts.navigationType,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_from(self: *NavigationCurrentEntryChangeEvent) *NavigationHistoryEntry {
|
||||
return self.from;
|
||||
}
|
||||
|
||||
pub fn get_navigationType(self: *const NavigationCurrentEntryChangeEvent) ?NavigationType {
|
||||
return self.navigation_type;
|
||||
}
|
||||
|
||||
pub fn dispatch(navigation: *Navigation, from: *NavigationHistoryEntry, typ: ?NavigationType) void {
|
||||
log.debug(.script_event, "dispatch event", .{
|
||||
.type = "currententrychange",
|
||||
.source = "navigation",
|
||||
});
|
||||
|
||||
var evt = NavigationCurrentEntryChangeEvent.constructor(
|
||||
"currententrychange",
|
||||
.{ .from = from, .navigationType = typ },
|
||||
) catch |err| {
|
||||
log.err(.app, "event constructor error", .{
|
||||
.err = err,
|
||||
.type = "currententrychange",
|
||||
.source = "navigation",
|
||||
});
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
_ = parser.eventTargetDispatchEvent(
|
||||
@as(*parser.EventTarget, @ptrCast(navigation)),
|
||||
&evt.proto,
|
||||
) catch |err| {
|
||||
log.err(.app, "dispatch event error", .{
|
||||
.err = err,
|
||||
.type = "currententrychange",
|
||||
.source = "navigation",
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Navigation" {
|
||||
try testing.htmlRunner("html/navigation/navigation.html");
|
||||
try testing.htmlRunner("html/navigation/navigation_currententrychange.html");
|
||||
}
|
||||
@@ -559,6 +559,7 @@ pub const EventType = enum(u8) {
|
||||
message_event = 7,
|
||||
keyboard_event = 8,
|
||||
pop_state = 9,
|
||||
navigation_current_entry_change_event = 10,
|
||||
};
|
||||
|
||||
pub const MutationEvent = c.dom_mutation_event;
|
||||
@@ -830,6 +831,7 @@ pub const EventTargetTBase = extern struct {
|
||||
message_port = 7,
|
||||
screen = 8,
|
||||
screen_orientation = 9,
|
||||
navigation = 10,
|
||||
};
|
||||
|
||||
vtable: ?*const c.struct_dom_event_target_vtable = &c.struct_dom_event_target_vtable{
|
||||
|
||||
@@ -35,6 +35,9 @@ const ScriptManager = @import("ScriptManager.zig");
|
||||
const SlotChangeMonitor = @import("SlotChangeMonitor.zig");
|
||||
const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
||||
|
||||
const NavigationKind = @import("navigation/navigation.zig").NavigationKind;
|
||||
const NavigationCurrentEntryChangeEvent = @import("navigation/navigation.zig").NavigationCurrentEntryChangeEvent;
|
||||
|
||||
const js = @import("js/js.zig");
|
||||
const URL = @import("../url.zig").URL;
|
||||
|
||||
@@ -815,8 +818,8 @@ pub const Page = struct {
|
||||
},
|
||||
}
|
||||
|
||||
// Push the navigation after a successful load.
|
||||
try self.session.history.pushNavigation(self.url.raw, self);
|
||||
// We need to handle different navigation types differently.
|
||||
try self.session.navigation.processNavigation(self);
|
||||
}
|
||||
|
||||
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
@@ -906,7 +909,7 @@ pub const Page = struct {
|
||||
.a => {
|
||||
const element: *parser.Element = @ptrCast(node);
|
||||
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
|
||||
try self.navigateFromWebAPI(href, .{});
|
||||
try self.navigateFromWebAPI(href, .{}, .{ .push = null });
|
||||
},
|
||||
.input => {
|
||||
const element: *parser.Element = @ptrCast(node);
|
||||
@@ -1018,8 +1021,25 @@ pub const Page = struct {
|
||||
// As such we schedule the function to be called as soon as possible.
|
||||
// The page.arena is safe to use here, but the transfer_arena exists
|
||||
// specifically for this type of lifetime.
|
||||
pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts) !void {
|
||||
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 });
|
||||
|
||||
// Force will force a page load.
|
||||
// Otherwise, we need to check if this is a true navigation.
|
||||
if (!opts.force) {
|
||||
// If we are navigating within the same document, just change URL.
|
||||
const new_url = try URL.parse(stitched_url, null);
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (session.queued_navigation != null) {
|
||||
// It might seem like this should never happen. And it might not,
|
||||
// BUT..consider the case where we have script like:
|
||||
@@ -1042,9 +1062,11 @@ pub const Page = struct {
|
||||
|
||||
session.queued_navigation = .{
|
||||
.opts = opts,
|
||||
.url = try URL.stitch(session.transfer_arena, url, self.url.raw, .{ .alloc = .always }),
|
||||
.url = stitched_url,
|
||||
};
|
||||
|
||||
session.navigation_kind = kind;
|
||||
|
||||
self.http_client.abort();
|
||||
|
||||
// In v8, this throws an exception which JS code cannot catch.
|
||||
@@ -1095,7 +1117,7 @@ pub const Page = struct {
|
||||
} else {
|
||||
action = try URL.concatQueryString(transfer_arena, action, buf.items);
|
||||
}
|
||||
try self.navigateFromWebAPI(action, opts);
|
||||
try self.navigateFromWebAPI(action, opts, .{ .push = null });
|
||||
}
|
||||
|
||||
pub fn isNodeAttached(self: *const Page, node: *parser.Node) bool {
|
||||
@@ -1153,6 +1175,7 @@ pub const NavigateReason = enum {
|
||||
form,
|
||||
script,
|
||||
history,
|
||||
navigation,
|
||||
};
|
||||
|
||||
pub const NavigateOpts = struct {
|
||||
@@ -1161,6 +1184,7 @@ pub const NavigateOpts = struct {
|
||||
method: Http.Method = .GET,
|
||||
body: ?[]const u8 = null,
|
||||
header: ?[:0]const u8 = null,
|
||||
force: bool = false,
|
||||
};
|
||||
|
||||
const IdleNotification = union(enum) {
|
||||
|
||||
@@ -22,9 +22,11 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const js = @import("js/js.zig");
|
||||
const Page = @import("page.zig").Page;
|
||||
const NavigationKind = @import("navigation/navigation.zig").NavigationKind;
|
||||
const Browser = @import("browser.zig").Browser;
|
||||
const NavigateOpts = @import("page.zig").NavigateOpts;
|
||||
const History = @import("html/History.zig");
|
||||
const Navigation = @import("navigation/Navigation.zig");
|
||||
|
||||
const log = @import("../log.zig");
|
||||
const parser = @import("netsurf.zig");
|
||||
@@ -57,6 +59,8 @@ pub const Session = struct {
|
||||
// History is persistent across the "tab".
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/History
|
||||
history: History = .{},
|
||||
navigation: Navigation = .{},
|
||||
navigation_kind: ?NavigationKind = null,
|
||||
|
||||
page: ?Page = null,
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
||||
var cdp = bc.cdp;
|
||||
const reason_: ?[]const u8 = switch (event.opts.reason) {
|
||||
.anchor => "anchorClick",
|
||||
.script, .history => "scriptInitiated",
|
||||
.script, .history, .navigation => "scriptInitiated",
|
||||
.form => switch (event.opts.method) {
|
||||
.GET => "formSubmissionGet",
|
||||
.POST => "formSubmissionPost",
|
||||
|
||||
@@ -402,19 +402,13 @@ pub fn htmlRunner(file: []const u8) !void {
|
||||
|
||||
const url = try std.fmt.allocPrint(arena_allocator, "http://localhost:9582/src/tests/{s}", .{file});
|
||||
try page.navigate(url, .{});
|
||||
_ = page.wait(2000);
|
||||
test_session.fetchWait(2000);
|
||||
|
||||
// page exits more aggressively in tests. We want to make sure this is called
|
||||
// at lease once.
|
||||
page.session.browser.runMicrotasks();
|
||||
page.session.browser.runMessageLoop();
|
||||
|
||||
const needs_second_wait = try js_context.exec("testing._onPageWait.length > 0", "check_onPageWait");
|
||||
if (needs_second_wait.value.toBool(page.js.isolate)) {
|
||||
// sets the isSecondWait flag in testing.
|
||||
_ = js_context.exec("testing._isSecondWait = true", "set_second_wait_flag") catch {};
|
||||
_ = page.wait(2000);
|
||||
}
|
||||
|
||||
@import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start);
|
||||
|
||||
const value = js_context.exec("testing.getStatus()", "testing.getStatus()") catch |err| {
|
||||
|
||||
@@ -326,3 +326,16 @@
|
||||
testing.expectEqual("after begin", newElement.innerText);
|
||||
testing.expectEqual("afterbegin", newElement.className);
|
||||
</script>
|
||||
|
||||
<script id=nonBreakingSpace>
|
||||
// Test non-breaking space encoding (critical for React hydration)
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = 'hello\xa0world';
|
||||
testing.expectEqual('hello\xa0world', div.textContent);
|
||||
testing.expectEqual('hello world', div.innerHTML);
|
||||
|
||||
// Test that outerHTML also encodes non-breaking spaces correctly
|
||||
const p = document.createElement('p');
|
||||
p.textContent = 'XAnge\xa0Privacy';
|
||||
testing.expectEqual('<p>XAnge Privacy</p>', p.outerHTML);
|
||||
</script>
|
||||
|
||||
@@ -224,3 +224,22 @@ let first_child = content.firstChild.nextSibling; // nextSibling because of line
|
||||
testing.expectEqual(6, Node.ENTITY_NODE);
|
||||
testing.expectEqual(12, Node.NOTATION_NODE);
|
||||
</script>
|
||||
|
||||
<span id=token class="token" style="color:#ce9178">"puppeteer "</span>
|
||||
<h3 id=name>Leto
|
||||
<!-- -->
|
||||
<!-- -->
|
||||
Atreides</h3>
|
||||
<script id=normalize>
|
||||
const token = $('#token');
|
||||
testing.expectEqual('"puppeteer "', token.firstChild.nodeValue);
|
||||
|
||||
const name = $('#name');
|
||||
testing.expectEqual([
|
||||
"Leto\n ",
|
||||
" ",
|
||||
"\n ",
|
||||
" ",
|
||||
"\n Atreides"
|
||||
], Array.from(name.childNodes).map((n) => n.nodeValue));
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<!-- Test fixture -->
|
||||
<div id="container">
|
||||
<!-- comment1 -->
|
||||
<div id="outer">
|
||||
<!-- comment2 -->
|
||||
<span id="inner">
|
||||
<!-- comment3 -->
|
||||
Text content
|
||||
<!-- comment4 -->
|
||||
</span>
|
||||
<!-- comment5 -->
|
||||
</div>
|
||||
<!-- comment6 -->
|
||||
</div>
|
||||
|
||||
<script id=nodeFilter>
|
||||
testing.expectEqual(1, NodeFilter.FILTER_ACCEPT);
|
||||
testing.expectEqual(2, NodeFilter.FILTER_REJECT);
|
||||
@@ -7,3 +23,197 @@
|
||||
testing.expectEqual(4294967295, NodeFilter.SHOW_ALL);
|
||||
testing.expectEqual(129, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT);
|
||||
</script>
|
||||
|
||||
<script id=treeWalkerComments>
|
||||
{
|
||||
const container = $('#container');
|
||||
const walker = document.createTreeWalker(
|
||||
container,
|
||||
NodeFilter.SHOW_COMMENT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
const comments = [];
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
comments.push(node.data.trim());
|
||||
}
|
||||
|
||||
// Should find all 6 comments, including those nested inside elements
|
||||
testing.expectEqual(6, comments.length);
|
||||
testing.expectEqual('comment1', comments[0]);
|
||||
testing.expectEqual('comment2', comments[1]);
|
||||
testing.expectEqual('comment3', comments[2]);
|
||||
testing.expectEqual('comment4', comments[3]);
|
||||
testing.expectEqual('comment5', comments[4]);
|
||||
testing.expectEqual('comment6', comments[5]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=treeWalkerElements>
|
||||
{
|
||||
const container = $('#container');
|
||||
const walker = document.createTreeWalker(
|
||||
container,
|
||||
NodeFilter.SHOW_ELEMENT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
const elements = [];
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
if (node.id) {
|
||||
elements.push(node.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Should find the 2 nested elements (outer and inner)
|
||||
testing.expectEqual(2, elements.length);
|
||||
testing.expectEqual('outer', elements[0]);
|
||||
testing.expectEqual('inner', elements[1]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=treeWalkerAll>
|
||||
{
|
||||
const container = $('#container');
|
||||
const walker = document.createTreeWalker(
|
||||
container,
|
||||
NodeFilter.SHOW_ALL,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
let commentCount = 0;
|
||||
let elementCount = 0;
|
||||
let textCount = 0;
|
||||
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
if (node.nodeType === 8) commentCount++; // Comment
|
||||
else if (node.nodeType === 1) elementCount++; // Element
|
||||
else if (node.nodeType === 3) textCount++; // Text
|
||||
}
|
||||
|
||||
testing.expectEqual(6, commentCount);
|
||||
testing.expectEqual(2, elementCount);
|
||||
testing.expectEqual(true, textCount > 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=treeWalkerCombined>
|
||||
{
|
||||
const container = $('#container');
|
||||
const walker = document.createTreeWalker(
|
||||
container,
|
||||
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
let commentCount = 0;
|
||||
let elementCount = 0;
|
||||
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
if (node.nodeType === 8) commentCount++; // Comment
|
||||
else if (node.nodeType === 1) elementCount++; // Element
|
||||
}
|
||||
|
||||
// Should find 6 comments and 2 elements, but no text nodes
|
||||
testing.expectEqual(6, commentCount);
|
||||
testing.expectEqual(2, elementCount);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=treeWalkerCustomFilter>
|
||||
{
|
||||
const container = $('#container');
|
||||
|
||||
// Filter that accepts only elements with id
|
||||
const walker = document.createTreeWalker(
|
||||
container,
|
||||
NodeFilter.SHOW_ELEMENT,
|
||||
{
|
||||
acceptNode: function(node) {
|
||||
return node.id ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const elements = [];
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
elements.push(node.id);
|
||||
}
|
||||
|
||||
// Should find only elements with id (outer and inner)
|
||||
testing.expectEqual(2, elements.length);
|
||||
testing.expectEqual('outer', elements[0]);
|
||||
testing.expectEqual('inner', elements[1]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=nodeIteratorComments>
|
||||
{
|
||||
const container = $('#container');
|
||||
const iterator = document.createNodeIterator(
|
||||
container,
|
||||
NodeFilter.SHOW_COMMENT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
const comments = [];
|
||||
let node;
|
||||
while (node = iterator.nextNode()) {
|
||||
comments.push(node.data.trim());
|
||||
}
|
||||
|
||||
// Should find all 6 comments, including those nested inside elements
|
||||
testing.expectEqual(6, comments.length);
|
||||
testing.expectEqual('comment1', comments[0]);
|
||||
testing.expectEqual('comment2', comments[1]);
|
||||
testing.expectEqual('comment3', comments[2]);
|
||||
testing.expectEqual('comment4', comments[3]);
|
||||
testing.expectEqual('comment5', comments[4]);
|
||||
testing.expectEqual('comment6', comments[5]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=reactLikeScenario>
|
||||
{
|
||||
// Test a React-like scenario with comment markers
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `
|
||||
<a href="/">
|
||||
<!--$-->
|
||||
<svg viewBox="0 0 10 10">
|
||||
<path d="M0,0 L10,10" />
|
||||
</svg>
|
||||
<!--/$-->
|
||||
</a>
|
||||
`;
|
||||
|
||||
const walker = document.createTreeWalker(
|
||||
div,
|
||||
NodeFilter.SHOW_COMMENT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
const comments = [];
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
comments.push(node.data);
|
||||
}
|
||||
|
||||
// Should find both React markers even though they're nested inside <a>
|
||||
testing.expectEqual(2, comments.length);
|
||||
testing.expectEqual('$', comments[0]);
|
||||
testing.expectEqual('/$', comments[1]);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<script id=history>
|
||||
testing.expectEqual('auto', history.scrollRestoration);
|
||||
|
||||
history.scrollRestoration = 'manual';
|
||||
history.scrollRestoration = 'foo';
|
||||
testing.expectEqual('manual', history.scrollRestoration);
|
||||
|
||||
history.scrollRestoration = 'auto';
|
||||
testing.expectEqual('auto', history.scrollRestoration);
|
||||
testing.expectEqual(null, history.state)
|
||||
|
||||
history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/xhr/json');
|
||||
history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/src/tests/html/history/history_after_nav.html');
|
||||
testing.expectEqual({ testInProgress: true }, history.state);
|
||||
|
||||
history.pushState({ testInProgress: false }, null, 'http://127.0.0.1:9582/xhr/json');
|
||||
history.replaceState({ "new": "field", testComplete: true }, null);
|
||||
|
||||
let state = { "new": "field", testComplete: true };
|
||||
testing.expectEqual(state, history.state);
|
||||
|
||||
@@ -32,10 +33,5 @@
|
||||
testing.expectEqual(state, popstateEventState);
|
||||
})
|
||||
|
||||
testing.onPageWait(() => {
|
||||
testing.expectEqual(true, history.state && history.state.testComplete);
|
||||
testing.expectEqual(state, history.state);
|
||||
});
|
||||
|
||||
testing.expectEqual(undefined, history.go());
|
||||
history.back();
|
||||
</script>
|
||||
26
src/tests/html/history/history2.html
Normal file
26
src/tests/html/history/history2.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<script id=history2>
|
||||
history.pushState(
|
||||
{"new": "field", testComplete: true },
|
||||
null,
|
||||
'http://127.0.0.1:9582/src/tests/html/history/history_after_nav.html'
|
||||
);
|
||||
|
||||
let popstateEventFired = false;
|
||||
let popstateEventState = null;
|
||||
|
||||
// uses the window event listener.
|
||||
window.onpopstate = (event) => {
|
||||
popstateEventFired = true;
|
||||
popstateEventState = event.state;
|
||||
};
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(true, popstateEventFired);
|
||||
testing.expectEqual(true, popstateEventState.testComplete);
|
||||
})
|
||||
|
||||
history.back();
|
||||
</script>
|
||||
6
src/tests/html/history/history_after_nav.html
Normal file
6
src/tests/html/history/history_after_nav.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<script id=history2>
|
||||
testing.expectEqual(true, history.state && history.state.testInProgress);
|
||||
</script>
|
||||
18
src/tests/html/navigation/navigation.html
Normal file
18
src/tests/html/navigation/navigation.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<script id=navigation>
|
||||
testing.expectEqual('object', typeof navigation);
|
||||
testing.expectEqual('object', typeof navigation.currentEntry);
|
||||
|
||||
testing.expectEqual('string', typeof navigation.currentEntry.id);
|
||||
testing.expectEqual('string', typeof navigation.currentEntry.key);
|
||||
testing.expectEqual('string', typeof navigation.currentEntry.url);
|
||||
|
||||
const currentIndex = navigation.currentEntry.index;
|
||||
|
||||
navigation.navigate(
|
||||
'http://localhost:9582/src/tests/html/navigation/navigation2.html',
|
||||
{ state: { currentIndex: currentIndex, navTestInProgress: true } }
|
||||
);
|
||||
</script>
|
||||
8
src/tests/html/navigation/navigation2.html
Normal file
8
src/tests/html/navigation/navigation2.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<script id=navigation2>
|
||||
const state = navigation.currentEntry.getState();
|
||||
testing.expectEqual(true, state.navTestInProgress);
|
||||
testing.expectEqual(state.currentIndex + 1, navigation.currentEntry.index);
|
||||
</script>
|
||||
15
src/tests/html/navigation/navigation_currententrychange.html
Normal file
15
src/tests/html/navigation/navigation_currententrychange.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<script id=navigation_currententrychange>
|
||||
let currentEntryChanged = false;
|
||||
|
||||
navigation.addEventListener("currententrychange", () => {
|
||||
currentEntryChanged = true;
|
||||
});
|
||||
|
||||
// Doesn't fully navigate if same document.
|
||||
location.href = location.href + "#1";
|
||||
|
||||
testing.expectEqual(true, currentEntryChanged);
|
||||
</script>
|
||||
@@ -1,19 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<svg width="200" height="100" style="border:1px solid #ccc" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">
|
||||
<svg id=lower width="200" height="100" style="border:1px solid #ccc" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">
|
||||
<rect></rect>
|
||||
<text x="100" y="95" font-size="14" text-anchor="middle">OVER 9000!!</text>
|
||||
</svg>
|
||||
|
||||
<SVG ID=UPPER WIDTH="200" HEIGHT="100" STYLE="BORDER:1PX SOLID #CCC" XMLNS="http://www.w3.org/2000/svg" VIEWBOX="0 0 200 100">
|
||||
<RECT></RECT>
|
||||
<TEXT X="100" Y="95" FONT-SIZE="14" TEXT-ANCHOR="MIDDLE">OVER 9000!!!</TEXT>
|
||||
</SVG>
|
||||
|
||||
<script id=svg>
|
||||
testing.expectEqual(false, 'AString' instanceof SVGElement);
|
||||
|
||||
const svg = document.querySelector('svg');
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg.getAttribute('xmlns'));
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg.getAttributeNode('xmlns').value);
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg.attributes.getNamedItem('xmlns').value);
|
||||
testing.expectEqual('0 0 200 100', svg.getAttribute('viewBox'));
|
||||
testing.expectEqual('viewBox', svg.getAttributeNode('viewBox').name);
|
||||
testing.expectEqual(true, svg.outerHTML.includes('viewBox'));
|
||||
const svg1 = $('#lower');
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg1.getAttribute('xmlns'));
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg1.getAttributeNode('xmlns').value);
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg1.attributes.getNamedItem('xmlns').value);
|
||||
testing.expectEqual('0 0 200 100', svg1.getAttribute('viewBox'));
|
||||
testing.expectEqual('viewBox', svg1.getAttributeNode('viewBox').name);
|
||||
testing.expectEqual(true, svg1.outerHTML.includes('viewBox'));
|
||||
testing.expectEqual('svg', svg1.tagName);
|
||||
testing.expectEqual('rect', svg1.querySelector('rect').tagName);
|
||||
testing.expectEqual('text', svg1.querySelector('text').tagName);
|
||||
|
||||
const svg2 = $('#UPPER');
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg2.getAttribute('xmlns'));
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg2.getAttributeNode('xmlns').value);
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg2.attributes.getNamedItem('xmlns').value);
|
||||
testing.expectEqual('0 0 200 100', svg2.getAttribute('viewBox'));
|
||||
testing.expectEqual('viewBox', svg2.getAttributeNode('viewBox').name);
|
||||
testing.expectEqual(true, svg2.outerHTML.includes('viewBox'));
|
||||
testing.expectEqual('svg', svg2.tagName);
|
||||
testing.expectEqual('rect', svg2.querySelector('rect').tagName);
|
||||
testing.expectEqual('text', svg2.querySelector('text').tagName);
|
||||
</script>
|
||||
|
||||
@@ -51,14 +51,6 @@
|
||||
// if we're already in a fail state, return fail, nothing can recover this
|
||||
if (testing._status === 'fail') return 'fail';
|
||||
|
||||
if (testing._isSecondWait) {
|
||||
for (const pw of (testing._onPageWait)) {
|
||||
testing._captured = pw[1];
|
||||
pw[0]();
|
||||
testing._captured = null;
|
||||
}
|
||||
}
|
||||
|
||||
// run any eventually's that we've captured
|
||||
for (const ev of testing._eventually) {
|
||||
testing._captured = ev[1];
|
||||
@@ -101,18 +93,6 @@
|
||||
_registerErrorCallback();
|
||||
}
|
||||
|
||||
// Set expectations to happen on the next time that `page.wait` is executed.
|
||||
//
|
||||
// History specifically uses this as it queues navigation that needs to be checked
|
||||
// when the next page is loaded.
|
||||
function onPageWait(fn) {
|
||||
// Store callbacks to run when page.wait() happens
|
||||
testing._onPageWait.push([fn, {
|
||||
script_id: document.currentScript.id,
|
||||
stack: new Error().stack,
|
||||
}]);
|
||||
}
|
||||
|
||||
async function async(promise, cb) {
|
||||
const script_id = document.currentScript ? document.currentScript.id : '<script id is unavailable in browsers>';
|
||||
const stack = new Error().stack;
|
||||
@@ -192,15 +172,12 @@
|
||||
window.testing = {
|
||||
_status: 'empty',
|
||||
_eventually: [],
|
||||
_onPageWait: [],
|
||||
_executed_scripts: new Set(),
|
||||
_captured: null,
|
||||
_isSecondWait: false,
|
||||
skip: skip,
|
||||
async: async,
|
||||
getStatus: getStatus,
|
||||
eventually: eventually,
|
||||
onPageWait: onPageWait,
|
||||
expectEqual: expectEqual,
|
||||
expectError: expectError,
|
||||
withError: withError,
|
||||
|
||||
71
src/url.zig
71
src/url.zig
@@ -217,6 +217,18 @@ pub const URL = struct {
|
||||
buf.appendSliceAssumeCapacity(query_string);
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
// Compares two URLs, returning true if it is the same document.
|
||||
pub fn eqlDocument(self: *const URL, other: *const URL, arena: Allocator) !bool {
|
||||
if (!std.mem.eql(u8, self.scheme(), other.scheme())) return false;
|
||||
if (!std.mem.eql(u8, self.host(), other.host())) return false;
|
||||
if (self.port() != other.port()) return false;
|
||||
|
||||
const path1 = try self.uri.path.toRawMaybeAlloc(arena);
|
||||
const path2 = try other.uri.path.toRawMaybeAlloc(arena);
|
||||
|
||||
return std.mem.eql(u8, path1, path2);
|
||||
}
|
||||
};
|
||||
|
||||
const StitchOpts = struct {
|
||||
@@ -553,3 +565,62 @@ test "URL: concatQueryString" {
|
||||
try testing.expectEqual("https://www.lightpanda.io/index?1=2&a=b", url);
|
||||
}
|
||||
}
|
||||
|
||||
test "URL: eqlDocument" {
|
||||
defer testing.reset();
|
||||
const arena = testing.arena_allocator;
|
||||
|
||||
{
|
||||
const url1 = try URL.parse("https://lightpanda.io/about", null);
|
||||
const url2 = try URL.parse("https://lightpanda.io/about", null);
|
||||
try testing.expectEqual(true, try url1.eqlDocument(&url2, arena));
|
||||
}
|
||||
|
||||
{
|
||||
const url1 = try URL.parse("https://lightpanda.io/about", null);
|
||||
const url2 = try URL.parse("http://lightpanda.io/about", null);
|
||||
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
|
||||
}
|
||||
|
||||
{
|
||||
const url1 = try URL.parse("https://lightpanda.io/about", null);
|
||||
const url2 = try URL.parse("https://example.com/about", null);
|
||||
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
|
||||
}
|
||||
|
||||
{
|
||||
const url1 = try URL.parse("https://lightpanda.io:8080/about", null);
|
||||
const url2 = try URL.parse("https://lightpanda.io:9090/about", null);
|
||||
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
|
||||
}
|
||||
|
||||
{
|
||||
const url1 = try URL.parse("https://lightpanda.io/about", null);
|
||||
const url2 = try URL.parse("https://lightpanda.io/contact", null);
|
||||
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
|
||||
}
|
||||
|
||||
{
|
||||
const url1 = try URL.parse("https://lightpanda.io/about?foo=bar", null);
|
||||
const url2 = try URL.parse("https://lightpanda.io/about?baz=qux", null);
|
||||
try testing.expectEqual(true, try url1.eqlDocument(&url2, arena));
|
||||
}
|
||||
|
||||
{
|
||||
const url1 = try URL.parse("https://lightpanda.io/about#section1", null);
|
||||
const url2 = try URL.parse("https://lightpanda.io/about#section2", null);
|
||||
try testing.expectEqual(true, try url1.eqlDocument(&url2, arena));
|
||||
}
|
||||
|
||||
{
|
||||
const url1 = try URL.parse("https://lightpanda.io/about", null);
|
||||
const url2 = try URL.parse("https://lightpanda.io/about/", null);
|
||||
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
|
||||
}
|
||||
|
||||
{
|
||||
const url1 = try URL.parse("https://lightpanda.io/", null);
|
||||
const url2 = try URL.parse("https://lightpanda.io", null);
|
||||
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
|
||||
}
|
||||
}
|
||||
|
||||
2
vendor/netsurf/libdom
vendored
2
vendor/netsurf/libdom
vendored
Submodule vendor/netsurf/libdom updated: ef7d5d4fab...c7f2d3cd27
2
vendor/netsurf/libhubbub
vendored
2
vendor/netsurf/libhubbub
vendored
Submodule vendor/netsurf/libhubbub updated: 6f102212c8...1624ba6250
Reference in New Issue
Block a user