2 Commits

Author SHA1 Message Date
Muki Kiboigo
55e9d8d166 use toEventTarget in NavigationEventTarget 2025-10-21 19:28:18 -07:00
Muki Kiboigo
d7d2f84794 not implemented on Navigation traverseTo 2025-10-21 19:26:41 -07:00
31 changed files with 410 additions and 810 deletions

View File

@@ -18,7 +18,7 @@ Lightpanda is the open-source browser made for headless usage:
- Javascript execution
- Support of Web APIs (partial, WIP)
- Compatible with Playwright[^1], Puppeteer, chromedp through [CDP](https://chromedevtools.github.io/devtools-protocol/)
- Compatible with Playwright[^1], Puppeteer, chromedp through CDP
Fast web automation for AI agents, LLM training, scraping and testing:
@@ -190,10 +190,10 @@ For systems with [Nix](https://nixos.org/download/), you can use the devShell:
nix develop
```
For MacOS, you need [Xcode](https://developer.apple.com/xcode/) and the following pacakges from homebrew:
For MacOS, you only need cmake:
```
brew install cmake pkgconf
brew install cmake
```
### Install and build dependencies

View File

@@ -384,7 +384,6 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
try buildMbedtls(b, mod);
try buildNghttp2(b, mod);
try buildCurl(b, mod);
try buildAda(b, mod);
switch (target.result.os.tag) {
.macos => {
@@ -850,34 +849,3 @@ fn buildCurl(b: *Build, m: *Build.Module) !void {
},
});
}
pub fn buildAda(b: *Build, m: *Build.Module) !void {
const ada_dep = b.dependency("ada-singleheader", .{});
const ada_mod = b.createModule(.{
.root_source_file = b.path("vendor/ada/root.zig"),
});
const ada_lib = b.addLibrary(.{
.name = "ada",
.root_module = b.createModule(.{
.link_libcpp = true,
.target = m.resolved_target,
.optimize = m.optimize,
}),
.linkage = .static,
});
ada_lib.addCSourceFile(.{
.file = ada_dep.path("ada.cpp"),
.flags = &.{ "-std=c++20", "-O3" },
.language = .cpp,
});
ada_lib.installHeader(ada_dep.path("ada_c.h"), "ada_c.h");
// Link the library to ada module.
ada_mod.linkLibrary(ada_lib);
// Expose ada module to main module.
m.addImport("ada", ada_mod);
}

View File

@@ -9,9 +9,5 @@
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
},
//.v8 = .{ .path = "../zig-v8-fork" }
.@"ada-singleheader" = .{
.url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
.hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
},
},
}

View File

@@ -228,9 +228,9 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
if (source == .@"inline") {
// if we're here, it means that we have pending scripts (i.e. self.scripts
// is not empty). Because the script is inline, it's complete/ready, but
// we need to process them in order.
// we need to process them in order
pending_script.complete = true;
pending_script.getList().append(&pending_script.node);
self.scripts.append(&pending_script.node);
return;
} else {
log.debug(.http, "script queue", .{
@@ -651,12 +651,6 @@ pub const PendingScript = struct {
return &self.manager.deferreds;
}
// Module scripts are deferred by default.
// https://v8.dev/features/modules#defer
if (script.kind == .module) {
return &self.manager.deferreds;
}
return &self.manager.scripts;
}
};

View File

@@ -39,7 +39,7 @@ const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
const CompositionEvent = @import("composition_event.zig").CompositionEvent;
const NavigationCurrentEntryChangeEvent = @import("../navigation/root.zig").NavigationCurrentEntryChangeEvent;
const NavigationCurrentEntryChangeEvent = @import("../navigation/navigation.zig").NavigationCurrentEntryChangeEvent;
// Event interfaces
pub const Interfaces = .{

View File

@@ -42,12 +42,12 @@ pub const HTMLDocument = struct {
// JS funcs
// --------
pub fn get_domain(self: *parser.DocumentHTML) ![]const u8 {
pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
// libdom's document_html get_domain always returns null, this is
// the way MDN recommends getting the domain anyways, since document.domain
// is deprecated.
const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
return location.get_host();
return location.get_host(page);
}
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {

View File

@@ -218,36 +218,36 @@ pub const HTMLAnchorElement = struct {
}
pub fn get_href(self: *parser.Anchor) ![]const u8 {
return parser.anchorGetHref(self);
return try parser.anchorGetHref(self);
}
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void {
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
return parser.anchorSetHref(self, full);
return try parser.anchorSetHref(self, full);
}
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
return parser.anchorGetHrefLang(self);
return try parser.anchorGetHrefLang(self);
}
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
return parser.anchorSetHrefLang(self, href);
return try parser.anchorSetHrefLang(self, href);
}
pub fn get_type(self: *parser.Anchor) ![]const u8 {
return parser.anchorGetType(self);
return try parser.anchorGetType(self);
}
pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
return parser.anchorSetType(self, t);
return try parser.anchorSetType(self, t);
}
pub fn get_rel(self: *parser.Anchor) ![]const u8 {
return parser.anchorGetRel(self);
return try parser.anchorGetRel(self);
}
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
return parser.anchorSetRel(self, t);
return try parser.anchorSetRel(self, t);
}
pub fn get_text(self: *parser.Anchor) !?[]const u8 {
@@ -269,175 +269,182 @@ pub const HTMLAnchorElement = struct {
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
}
return error.NotProvided;
return .empty;
}
// TODO return a disposable string
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
defer u.destructor();
return u.get_origin(page);
return try u.get_origin(page);
}
// TODO return a disposable string
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
defer u.destructor();
return page.call_arena.dupe(u8, u.get_protocol());
return u.get_protocol();
}
pub fn set_protocol(self: *parser.Anchor, protocol: []const u8, page: *Page) !void {
pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
defer u.destructor();
try u.set_protocol(protocol);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
u.uri.scheme = v;
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_host());
var u = try url(self, page);
return try u.get_host(page);
}
pub fn set_host(self: *parser.Anchor, host: []const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
try u.set_host(host);
pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void {
// search : separator
var p: ?u16 = null;
var h: []const u8 = undefined;
for (v, 0..) |c, i| {
if (c == ':') {
h = v[0..i];
p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
break;
}
}
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
const arena = page.arena;
var u = try url(self, page);
if (p) |pp| {
u.uri.host = .{ .raw = h };
u.uri.port = pp;
} else {
u.uri.host = .{ .raw = v };
u.uri.port = null;
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_hostname());
}
pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
try u.set_hostname(hostname);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
return u.get_hostname();
}
pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
u.uri.host = .{ .raw = v };
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_port());
var u = try url(self, page);
return try u.get_port(page);
}
pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void {
pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
defer u.destructor();
if (maybe_port) |port| {
try u.set_port(port);
if (v != null and v.?.len > 0) {
u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
} else {
u.clearPort();
u.uri.port = null;
}
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
var u = try url(self, page);
return u.get_username();
}
const username = u.get_username();
if (username.len == 0) {
return "";
pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
if (v) |vv| {
u.uri.user = .{ .raw = vv };
} else {
u.uri.user = null;
}
const href = try u.toString(arena);
return page.call_arena.dupe(u8, username);
}
pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
const username = if (maybe_username) |username| username else "";
try u.set_username(username);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_password());
}
pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
const password = if (maybe_password) |password| password else "";
try u.set_password(password);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
return try page.arena.dupe(u8, u.get_password());
}
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
if (v) |vv| {
u.uri.password = .{ .raw = vv };
} else {
u.uri.password = null;
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_pathname());
var u = try url(self, page);
return u.get_pathname();
}
pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void {
pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
defer u.destructor();
u.uri.path = .{ .raw = v };
const href = try u.toString(arena);
try u.set_pathname(pathname);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
try parser.anchorSetHref(self, href);
}
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
// This allocates in page arena so no need to dupe.
return u.get_search(page);
var u = try url(self, page);
return try u.get_search(page);
}
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
try u.set_search(v, page);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
const href = try u.toString(page.call_arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_hash());
var u = try url(self, page);
return try u.get_hash(page);
}
pub fn set_hash(self: *parser.Anchor, maybe_hash: ?[]const u8, page: *Page) !void {
pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
defer u.destructor();
if (maybe_hash) |hash| {
try u.set_hash(hash);
if (v) |vv| {
u.uri.fragment = .{ .raw = vv };
} else {
u.clearHash();
u.uri.fragment = null;
}
const href = try u.toString(arena);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
try parser.anchorSetHref(self, href);
}
};

View File

@@ -25,14 +25,13 @@ const URL = @import("../url/url.zig").URL;
pub const Location = struct {
url: URL,
/// Initializes the `Location` to be used in `Window`.
/// Browsers give such initial values when user not navigated yet:
/// Chrome -> chrome://new-tab-page/
/// Firefox -> about:newtab
/// Safari -> favorites://
pub fn init(url: []const u8) !Location {
return .{ .url = try .initForLocation(url) };
}
pub const default = Location{
.url = .initWithoutSearchParams(Uri.parse("about:blank") catch unreachable),
};
pub fn get_href(self: *Location, page: *Page) ![]const u8 {
return self.url.get_href(page);
@@ -46,16 +45,16 @@ pub const Location = struct {
return self.url.get_protocol();
}
pub fn get_host(self: *Location) []const u8 {
return self.url.get_host();
pub fn get_host(self: *Location, page: *Page) ![]const u8 {
return self.url.get_host(page);
}
pub fn get_hostname(self: *Location) []const u8 {
return self.url.get_hostname();
}
pub fn get_port(self: *Location) []const u8 {
return self.url.get_port();
pub fn get_port(self: *Location, page: *Page) ![]const u8 {
return self.url.get_port(page);
}
pub fn get_pathname(self: *Location) []const u8 {
@@ -66,8 +65,8 @@ pub const Location = struct {
return self.url.get_search(page);
}
pub fn get_hash(self: *Location) []const u8 {
return self.url.get_hash();
pub fn get_hash(self: *Location, page: *Page) ![]const u8 {
return self.url.get_hash(page);
}
pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
@@ -87,7 +86,7 @@ pub const Location = struct {
}
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
return self.get_href(page);
return try self.get_href(page);
}
};

View File

@@ -56,7 +56,7 @@ pub const Window = struct {
document: *parser.DocumentHTML,
target: []const u8 = "",
location: Location,
location: Location = .default,
storage_shelf: ?*storage.Shelf = null,
// counter for having unique timer ids
@@ -83,7 +83,6 @@ pub const Window = struct {
return .{
.document = html_doc,
.target = target orelse "",
.location = try .init("about:blank"),
.navigator = navigator orelse .{},
.performance = Performance.init(),
};
@@ -94,10 +93,6 @@ pub const Window = struct {
try parser.documentHTMLSetLocation(Location, self.document, &self.location);
}
pub fn changeLocation(self: *Window, new_url: []const u8, page: *Page) !void {
return self.location.url.reinit(new_url, page);
}
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
self.document = doc;

View File

@@ -16,7 +16,7 @@ const Interfaces = generate.Tuple(.{
@import("../storage/storage.zig").Interfaces,
@import("../url/url.zig").Interfaces,
@import("../xhr/xhr.zig").Interfaces,
@import("../navigation/root.zig").Interfaces,
@import("../navigation/navigation.zig").Interfaces,
@import("../xhr/form_data.zig").Interfaces,
@import("../xhr/File.zig"),
@import("../xmlserializer/xmlserializer.zig").Interfaces,

View File

@@ -32,13 +32,13 @@ const parser = @import("../netsurf.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/Navigation
const Navigation = @This();
const NavigationKind = @import("root.zig").NavigationKind;
const NavigationHistoryEntry = @import("root.zig").NavigationHistoryEntry;
const NavigationTransition = @import("root.zig").NavigationTransition;
const NavigationCurrentEntryChangeEvent = @import("root.zig").NavigationCurrentEntryChangeEvent;
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{},
@@ -268,9 +268,7 @@ pub const TraverseToOptions = struct {
};
pub fn _traverseTo(self: *Navigation, key: []const u8, _opts: ?TraverseToOptions, page: *Page) !NavigationReturn {
if (_opts != null) {
log.debug(.browser, "not implemented", .{ .options = _opts });
}
log.debug(.browser, "not implemented", .{ .options = _opts });
for (self.entries.items, 0..) |entry, i| {
if (std.mem.eql(u8, key, entry.key)) {

View File

@@ -52,7 +52,5 @@ pub fn set_oncurrententrychange(self: *NavigationEventTarget, listener: ?EventHa
if (self.oncurrententrychange_cbk) |cbk| try self.unregister("currententrychange", cbk.id);
if (listener) |listen| {
self.oncurrententrychange_cbk = try self.register(page.arena, "currententrychange", listen);
} else {
self.oncurrententrychange_cbk = null;
}
}

View File

@@ -88,7 +88,7 @@ pub const NavigationHistoryEntry = struct {
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.call_arena);
return page.url.eqlDocument(&url, page.arena);
}
pub fn get_url(self: *const NavigationHistoryEntry) ?[]const u8 {

View File

@@ -35,8 +35,8 @@ const ScriptManager = @import("ScriptManager.zig");
const SlotChangeMonitor = @import("SlotChangeMonitor.zig");
const HTMLDocument = @import("html/document.zig").HTMLDocument;
const NavigationKind = @import("navigation/root.zig").NavigationKind;
const NavigationCurrentEntryChangeEvent = @import("navigation/root.zig").NavigationCurrentEntryChangeEvent;
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;
@@ -488,16 +488,16 @@ pub const Page = struct {
}
{
std.debug.print("\nhigh_priority schedule: {d}\n", .{self.scheduler.high_priority.count()});
var it = self.scheduler.high_priority.iterator();
std.debug.print("\nprimary schedule: {d}\n", .{self.scheduler.primary.count()});
var it = self.scheduler.primary.iterator();
while (it.next()) |task| {
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now });
}
}
{
std.debug.print("\nlow_priority schedule: {d}\n", .{self.scheduler.low_priority.count()});
var it = self.scheduler.low_priority.iterator();
std.debug.print("\nsecondary schedule: {d}\n", .{self.scheduler.secondary.count()});
var it = self.scheduler.secondary.iterator();
while (it.next()) |task| {
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now });
}
@@ -552,31 +552,14 @@ pub const Page = struct {
.body = opts.body != null,
});
// if the url is about:blank, we load an empty HTML document in the
// page and dispatch the events.
// if the url is about:blank, nothing to do.
if (std.mem.eql(u8, "about:blank", request_url)) {
const html_doc = try parser.documentHTMLParseFromStr("");
try self.setDocument(html_doc);
// Assume we parsed the document.
// It's important to force a reset during the following navigation.
self.mode = .parsed;
// We do not processHTMLDoc here as we know we don't have any scripts
// This assumption may be false when CDP Page.addScriptToEvaluateOnNewDocument is implemented
self.documentIsComplete();
self.session.browser.notification.dispatch(.page_navigate, &.{
.opts = opts,
.url = request_url,
.timestamp = timestamp(),
});
self.session.browser.notification.dispatch(.page_navigated, &.{
.url = request_url,
.timestamp = timestamp(),
});
try HTMLDocument.documentIsComplete(self.window.document, self);
return;
}
@@ -879,7 +862,7 @@ pub const Page = struct {
self.window.setStorageShelf(
try self.session.storage_shed.getOrPut(try self.origin(self.arena)),
);
try self.window.changeLocation(self.url.raw, self);
try self.window.replaceLocation(.{ .url = try self.url.toWebApi(self.arena) });
}
pub const MouseEvent = struct {

View File

@@ -41,10 +41,6 @@ const FlatRenderer = struct {
const Element = @import("dom/element.zig").Element;
// Define the size of each element in the grid.
const default_w = 5;
const default_h = 5;
// we expect allocator to be an arena
pub fn init(allocator: Allocator) FlatRenderer {
return .{
@@ -66,10 +62,10 @@ const FlatRenderer = struct {
gop.value_ptr.* = x;
}
const _x: f64 = @floatFromInt(x * default_w);
const _x: f64 = @floatFromInt(x);
const y: f64 = 0.0;
const w: f64 = default_w;
const h: f64 = default_h;
const w: f64 = 1.0;
const h: f64 = 1.0;
return .{
.x = _x,
@@ -102,20 +98,18 @@ const FlatRenderer = struct {
}
pub fn width(self: *const FlatRenderer) u32 {
return @max(@as(u32, @intCast(self.elements.items.len * default_w)), default_w); // At least default width pixels even if empty
return @max(@as(u32, @intCast(self.elements.items.len)), 1); // At least 1 pixel even if empty
}
pub fn height(_: *const FlatRenderer) u32 {
return 5;
return 1;
}
pub fn getElementAtPosition(self: *const FlatRenderer, _x: i32, y: i32) ?*parser.Element {
if (y < 0 or y > default_h or _x < 0) {
pub fn getElementAtPosition(self: *const FlatRenderer, x: i32, y: i32) ?*parser.Element {
if (y != 0 or x < 0) {
return null;
}
const x = @divFloor(_x, default_w);
const elements = self.elements.items;
return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null;
}

View File

@@ -22,7 +22,7 @@ const Allocator = std.mem.Allocator;
const js = @import("js/js.zig");
const Page = @import("page.zig").Page;
const NavigationKind = @import("navigation/root.zig").NavigationKind;
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");

View File

@@ -18,8 +18,6 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const Writer = std.Io.Writer;
const ada = @import("ada");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig");
@@ -37,235 +35,182 @@ pub const Interfaces = .{
EntryIterable,
};
/// https://developer.mozilla.org/en-US/docs/Web/API/URL/URL
// https://url.spec.whatwg.org/#url
//
// TODO we could avoid many of these getter string allocatoration in two differents
// way:
//
// 1. We can eventually get the slice of scheme *with* the following char in
// the underlying string. But I don't know if it's possible and how to do that.
// I mean, if the rawuri contains `https://foo.bar`, uri.scheme is a slice
// containing only `https`. I want `https:` so, in theory, I don't need to
// allocatorate data, I should be able to retrieve the scheme + the following `:`
// from rawuri.
//
// 2. The other way would be to copy the `std.Uri` code to have a dedicated
// parser including the characters we want for the web API.
pub const URL = struct {
internal: ada.URL,
/// We prefer in-house search params solution here;
/// ada's search params impl use more memory.
/// It also offers it's own iterator implementation
/// where we'd like to use ours.
uri: std.Uri,
search_params: URLSearchParams,
pub const empty = URL{
.internal = null,
.uri = .{ .scheme = "" },
.search_params = .{},
};
// You can use an existing URL object for either argument, and it will be
// stringified from the object's href property.
const ConstructorArg = union(enum) {
const URLArg = union(enum) {
url: *URL,
element: *parser.ElementHTML,
string: []const u8,
url: *const URL,
element: *parser.Element,
fn toString(self: ConstructorArg, page: *Page) ![]const u8 {
return switch (self) {
.string => |s| s,
.url => |url| url._toString(page),
.element => |e| {
const attrib = try parser.elementGetAttribute(@ptrCast(e), "href") orelse {
return error.InvalidArgument;
};
return attrib;
},
};
fn toString(self: URLArg, arena: Allocator) !?[]const u8 {
switch (self) {
.string => |s| return s,
.url => |url| return try url.toString(arena),
.element => |e| return try parser.elementGetAttribute(@ptrCast(e), "href"),
}
}
};
pub fn constructor(url: ConstructorArg, maybe_base: ?ConstructorArg, page: *Page) !URL {
const url_str = try url.toString(page);
pub fn constructor(url: URLArg, base: ?URLArg, page: *Page) !URL {
const arena = page.arena;
const url_str = try url.toString(arena) orelse return error.InvalidArgument;
const internal = try blk: {
if (maybe_base) |base| {
break :blk ada.parseWithBase(url_str, try base.toString(page));
var raw: ?[]const u8 = null;
if (base) |b| {
if (try b.toString(arena)) |bb| {
raw = try @import("../../url.zig").URL.stitch(arena, url_str, bb, .{});
}
}
break :blk ada.parse(url_str);
if (raw == null) {
// if it was a URL, then it's already be owned by the arena
raw = if (url == .url) url_str else try arena.dupe(u8, url_str);
}
const uri = std.Uri.parse(raw.?) catch blk: {
if (!std.mem.endsWith(u8, raw.?, "://")) {
return error.TypeError;
}
// schema only is valid!
break :blk std.Uri{
.scheme = raw.?[0 .. raw.?.len - 3],
.host = .{ .percent_encoded = "" },
};
};
return init(arena, uri);
}
pub fn init(arena: Allocator, uri: std.Uri) !URL {
return .{
.internal = internal,
.search_params = try prepareSearchParams(page.arena, internal),
.uri = uri,
.search_params = try URLSearchParams.init(
arena,
uriComponentNullStr(uri.query),
),
};
}
pub fn destructor(self: *const URL) void {
// Not tracked by arena.
return ada.free(self.internal);
pub fn initWithoutSearchParams(uri: std.Uri) URL {
return .{ .uri = uri, .search_params = .{} };
}
/// Only to be used by `Location` API. `url` MUST NOT provide search params.
pub fn initForLocation(url: []const u8) !URL {
return .{ .internal = try ada.parse(url), .search_params = .{} };
pub fn get_origin(self: *URL, page: *Page) ![]const u8 {
var aw = std.Io.Writer.Allocating.init(page.arena);
try self.uri.writeToStream(&aw.writer, .{
.scheme = true,
.authentication = false,
.authority = true,
.path = false,
.query = false,
.fragment = false,
});
return aw.written();
}
/// Reinitializes the URL by parsing given `url`. Search params can be provided.
pub fn reinit(self: *URL, url: []const u8, page: *Page) !void {
_ = ada.setHref(self.internal, url);
if (!ada.isValid(self.internal)) return error.Internal;
self.search_params = try prepareSearchParams(page.arena, self.internal);
// get_href returns the URL by writing all its components.
pub fn get_href(self: *URL, page: *Page) ![]const u8 {
return self.toString(page.arena);
}
/// Prepares a `URLSearchParams` from given `internal`.
/// Resets `search` of `internal`.
fn prepareSearchParams(arena: Allocator, internal: ada.URL) !URLSearchParams {
const maybe_search = ada.getSearchNullable(internal);
// Empty.
if (maybe_search.data == null) return .{};
const search = maybe_search.data[0..maybe_search.length];
const search_params = URLSearchParams.initFromString(arena, search);
// After a call to this function, search params are tracked by
// `search_params`. So we reset the internal's search.
ada.clearSearch(internal);
return search_params;
pub fn _toString(self: *URL, page: *Page) ![]const u8 {
return self.toString(page.arena);
}
pub fn clearPort(self: *const URL) void {
return ada.clearPort(self.internal);
}
// format the url with all its components.
pub fn toString(self: *const URL, arena: Allocator) ![]const u8 {
var aw = std.Io.Writer.Allocating.init(arena);
try self.uri.writeToStream(&aw.writer, .{
.scheme = true,
.authentication = true,
.authority = true,
.path = uriComponentNullStr(self.uri.path).len > 0,
});
pub fn clearHash(self: *const URL) void {
return ada.clearHash(self.internal);
}
/// Returns a boolean indicating whether or not an absolute URL,
/// or a relative URL combined with a base URL, are parsable and valid.
pub fn static_canParse(url: ConstructorArg, maybe_base: ?ConstructorArg, page: *Page) !bool {
const url_str = try url.toString(page);
if (maybe_base) |base| {
return ada.canParseWithBase(url_str, try base.toString(page));
}
return ada.canParse(url_str);
}
/// Alias to get_href.
pub fn _toString(self: *const URL, page: *Page) ![]const u8 {
return self.get_href(page);
}
// Getters.
pub fn get_searchParams(self: *URL) *URLSearchParams {
return &self.search_params;
}
pub fn get_origin(self: *const URL, page: *Page) ![]const u8 {
// `ada.getOriginNullable` allocates memory in order to find the `origin`.
// We'd like to use our arena allocator for such case;
// so here we allocate the `origin` in page arena and free the original.
const maybe_origin = ada.getOriginNullable(self.internal);
if (maybe_origin.data == null) {
return "";
}
defer ada.freeOwnedString(maybe_origin);
const origin = maybe_origin.data[0..maybe_origin.length];
return page.call_arena.dupe(u8, origin);
}
pub fn get_href(self: *const URL, page: *Page) ![]const u8 {
var w: Writer.Allocating = .init(page.arena);
// If URL is not valid, return immediately.
if (!ada.isValid(self.internal)) {
return "";
}
// Since the earlier check passed, this can't be null.
const str = ada.getHrefNullable(self.internal);
const href = str.data[0..str.length];
// This can't be null either.
const comps = ada.getComponents(self.internal);
// If hash provided, we write it after we fit-in the search params.
const has_hash = comps.hash_start != ada.URLOmitted;
const href_part = if (has_hash) href[0..comps.hash_start] else href;
try w.writer.writeAll(href_part);
// Write search params if provided.
if (self.search_params.get_size() > 0) {
try w.writer.writeByte('?');
try self.search_params.write(&w.writer);
try aw.writer.writeByte('?');
try self.search_params.write(&aw.writer);
}
// Write hash if provided before.
const hash = self.get_hash();
try w.writer.writeAll(hash);
return w.written();
}
pub fn get_username(self: *const URL) []const u8 {
const username = ada.getUsernameNullable(self.internal);
if (username.data == null) {
return "";
{
const fragment = uriComponentNullStr(self.uri.fragment);
if (fragment.len > 0) {
try aw.writer.writeByte('#');
try aw.writer.writeAll(fragment);
}
}
return username.data[0..username.length];
return aw.written();
}
pub fn get_password(self: *const URL) []const u8 {
const password = ada.getPasswordNullable(self.internal);
if (password.data == null) {
return "";
}
return password.data[0..password.length];
pub fn get_protocol(self: *const URL) []const u8 {
// std.Uri keeps a pointer to "https", "http" (scheme part) so we know
// its followed by ':'.
const scheme = self.uri.scheme;
return scheme.ptr[0 .. scheme.len + 1];
}
pub fn get_port(self: *const URL) []const u8 {
const port = ada.getPortNullable(self.internal);
if (port.data == null) {
return "";
}
return port.data[0..port.length];
pub fn get_username(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.user);
}
pub fn get_hash(self: *const URL) []const u8 {
const hash = ada.getHashNullable(self.internal);
if (hash.data == null) {
return "";
}
return hash.data[0..hash.length];
pub fn get_password(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.password);
}
pub fn get_host(self: *const URL) []const u8 {
const host = ada.getHostNullable(self.internal);
if (host.data == null) {
return "";
}
return host.data[0..host.length];
pub fn get_host(self: *URL, page: *Page) ![]const u8 {
var aw = std.Io.Writer.Allocating.init(page.arena);
try self.uri.writeToStream(&aw.writer, .{
.scheme = false,
.authentication = false,
.authority = true,
.path = false,
.query = false,
.fragment = false,
});
return aw.written();
}
pub fn get_hostname(self: *const URL) []const u8 {
const hostname = ada.getHostnameNullable(self.internal);
if (hostname.data == null) {
return "";
}
return hostname.data[0..hostname.length];
pub fn get_hostname(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.host);
}
pub fn get_pathname(self: *const URL) []const u8 {
const path = ada.getPathnameNullable(self.internal);
// Return a slash if path is null.
if (path.data == null) {
return "/";
}
pub fn get_port(self: *URL, page: *Page) ![]const u8 {
const arena = page.arena;
if (self.uri.port == null) return try arena.dupe(u8, "");
return path.data[0..path.length];
var aw = std.Io.Writer.Allocating.init(arena);
try aw.writer.printInt(self.uri.port.?, 10, .lower, .{});
return aw.written();
}
/// get_search depends on the current state of `search_params`.
pub fn get_search(self: *const URL, page: *Page) ![]const u8 {
pub fn get_pathname(self: *URL) []const u8 {
if (uriComponentStr(self.uri.path).len == 0) return "/";
return uriComponentStr(self.uri.path);
}
pub fn get_search(self: *URL, page: *Page) ![]const u8 {
const arena = page.arena;
if (self.search_params.get_size() == 0) {
@@ -278,104 +223,72 @@ pub const URL = struct {
return buf.items;
}
pub fn get_protocol(self: *const URL) []const u8 {
const protocol = ada.getProtocolNullable(self.internal);
if (protocol.data == null) {
return "";
}
return protocol.data[0..protocol.length];
}
// Setters.
/// Ada-url don't define any errors, so we just prefer one unified
/// `Internal` error for failing cases.
const SetterError = error{Internal};
pub fn set_href(self: *URL, input: []const u8, page: *Page) !void {
_ = ada.setHref(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
// Can't call `get_search` here since it uses `search_params`.
self.search_params = try prepareSearchParams(page.arena, self.internal);
}
pub fn set_host(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setHost(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_hostname(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setHostname(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_protocol(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setProtocol(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_username(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setUsername(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_password(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setPassword(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_port(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setPort(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_pathname(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setPathname(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_search(self: *URL, maybe_input: ?[]const u8, page: *Page) !void {
pub fn set_search(self: *URL, qs_: ?[]const u8, page: *Page) !void {
self.search_params = .{};
if (maybe_input) |input| {
self.search_params = try .initFromString(page.arena, input);
if (qs_) |qs| {
self.search_params = try URLSearchParams.init(page.arena, qs);
}
}
pub fn set_hash(self: *const URL, input: []const u8) !void {
ada.setHash(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
pub fn get_hash(self: *URL, page: *Page) ![]const u8 {
const arena = page.arena;
if (self.uri.fragment == null) return try arena.dupe(u8, "");
return try std.mem.concat(arena, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) });
}
pub fn get_searchParams(self: *URL) *URLSearchParams {
return &self.search_params;
}
pub fn _toJSON(self: *URL, page: *Page) ![]const u8 {
return self.get_href(page);
}
};
// uriComponentNullStr converts an optional std.Uri.Component to string value.
// The string value can be undecoded.
fn uriComponentNullStr(c: ?std.Uri.Component) []const u8 {
if (c == null) return "";
return uriComponentStr(c.?);
}
fn uriComponentStr(c: std.Uri.Component) []const u8 {
return switch (c) {
.raw => |v| v,
.percent_encoded => |v| v,
};
}
// https://url.spec.whatwg.org/#interface-urlsearchparams
pub const URLSearchParams = struct {
entries: kv.List = .{},
pub const ConstructorOptions = union(enum) {
query_string: []const u8,
const URLSearchParamsOpts = union(enum) {
qs: []const u8,
form_data: *const FormData,
object: js.Object,
js_obj: js.Object,
};
pub fn constructor(opts_: ?URLSearchParamsOpts, page: *Page) !URLSearchParams {
const opts = opts_ orelse return .{ .entries = .{} };
return switch (opts) {
.qs => |qs| init(page.arena, qs),
.form_data => |fd| .{ .entries = try fd.entries.clone(page.arena) },
.js_obj => |js_obj| {
const arena = page.arena;
var it = js_obj.nameIterator();
pub fn constructor(maybe_options: ?ConstructorOptions, page: *Page) !URLSearchParams {
const options = maybe_options orelse return .{};
const arena = page.arena;
return switch (options) {
.query_string => |string| .{ .entries = try parseQuery(arena, string) },
.form_data => |form_data| .{ .entries = try form_data.entries.clone(arena) },
.object => |object| {
var it = object.nameIterator();
var entries = kv.List{};
var entries: kv.List = .{};
try entries.ensureTotalCapacity(arena, it.count);
while (try it.next()) |js_name| {
const name = try js_name.toString(arena);
const js_value = try object.get(name);
const value = try js_value.toString(arena);
entries.appendOwnedAssumeCapacity(name, value);
const js_val = try js_obj.get(name);
entries.appendOwnedAssumeCapacity(
name,
try js_val.toString(arena),
);
}
return .{ .entries = entries };
@@ -383,9 +296,10 @@ pub const URLSearchParams = struct {
};
}
/// Initializes URLSearchParams from a query string.
pub fn initFromString(arena: Allocator, query_string: []const u8) !URLSearchParams {
return .{ .entries = try parseQuery(arena, query_string) };
pub fn init(arena: Allocator, qs_: ?[]const u8) !URLSearchParams {
return .{
.entries = if (qs_) |qs| try parseQuery(arena, qs) else .{},
};
}
pub fn get_size(self: *const URLSearchParams) u32 {

View File

@@ -38,22 +38,16 @@ const DEV_TOOLS_WINDOW_ID = 1923710101;
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
getVersion,
setPermission,
setWindowBounds,
resetPermissions,
grantPermissions,
getWindowForTarget,
setDownloadBehavior,
getWindowForTarget,
setWindowBounds,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
.getVersion => return getVersion(cmd),
.setPermission => return setPermission(cmd),
.setWindowBounds => return setWindowBounds(cmd),
.resetPermissions => return resetPermissions(cmd),
.grantPermissions => return grantPermissions(cmd),
.getWindowForTarget => return getWindowForTarget(cmd),
.setDownloadBehavior => return setDownloadBehavior(cmd),
.getWindowForTarget => return getWindowForTarget(cmd),
.setWindowBounds => return setWindowBounds(cmd),
}
}
@@ -95,21 +89,6 @@ fn setWindowBounds(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn grantPermissions(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn setPermission(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn resetPermissions(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
const testing = @import("../testing.zig");
test "cdp.browser: getVersion" {
var ctx = testing.context();

View File

@@ -663,11 +663,11 @@ test "cdp.dom: getBoxModel" {
.params = .{ .nodeId = 6 },
});
try ctx.expectSentResult(.{ .model = BoxModel{
.content = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
.padding = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
.border = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
.margin = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
.width = 5,
.height = 5,
.content = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
.padding = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
.border = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
.margin = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
.width = 1,
.height = 1,
} }, .{ .id = 5 });
}

View File

@@ -18,7 +18,6 @@
const std = @import("std");
const Page = @import("../../browser/page.zig").Page;
const timestampF = @import("../../datetime.zig").timestamp;
const Notification = @import("../../notification.zig").Notification;
const Allocator = std.mem.Allocator;
@@ -83,33 +82,11 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void {
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
if (params.enabled == false) {
if (params.enabled) {
try bc.lifecycleEventsEnable();
} else {
bc.lifecycleEventsDisable();
return cmd.sendResult(null, .{});
}
// Enable lifecycle events.
try bc.lifecycleEventsEnable();
// When we enable lifecycle events, we must dispatch events for all
// attached targets.
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
if (page.load_state == .complete) {
try sendPageLifecycle(bc, "DOMContentLoaded", timestampF());
try sendPageLifecycle(bc, "load", timestampF());
const http_active = page.http_client.active;
const total_network_activity = http_active + page.http_client.intercepted;
if (page.notified_network_almost_idle.check(total_network_activity <= 2)) {
try sendPageLifecycle(bc, "networkAlmostIdle", timestampF());
}
if (page.notified_network_idle.check(total_network_activity == 0)) {
try sendPageLifecycle(bc, "networkIdle", timestampF());
}
}
return cmd.sendResult(null, .{});
}

View File

@@ -109,7 +109,7 @@ fn disposeBrowserContext(cmd: anytype) !void {
fn createTarget(cmd: anytype) !void {
const params = (try cmd.params(struct {
url: []const u8 = "about:blank",
// url: []const u8,
// width: ?u64 = null,
// height: ?u64 = null,
browserContextId: ?[]const u8 = null,
@@ -167,7 +167,7 @@ fn createTarget(cmd: anytype) !void {
.targetInfo = TargetInfo{
.attached = false,
.targetId = target_id,
.title = params.url,
.title = "about:blank",
.browserContextId = bc.id,
.url = "about:blank",
},
@@ -178,10 +178,6 @@ fn createTarget(cmd: anytype) !void {
try doAttachtoTarget(cmd, target_id);
}
try page.navigate(params.url, .{
.reason = .address_bar,
});
try cmd.sendResult(.{
.targetId = target_id,
}, .{});
@@ -521,7 +517,7 @@ test "cdp.target: createTarget" {
{
var ctx = testing.context();
defer ctx.deinit();
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about:blank" } });
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about/blank" } });
// should create a browser context
const bc = ctx.cdp().browser_context.?;
@@ -533,7 +529,7 @@ test "cdp.target: createTarget" {
defer ctx.deinit();
// active auto attach to get the Target.attachedToTarget event.
try ctx.processMessage(.{ .id = 9, .method = "Target.setAutoAttach", .params = .{ .autoAttach = true, .waitForDebuggerOnStart = false } });
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about:blank" } });
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about/blank" } });
// should create a browser context
const bc = ctx.cdp().browser_context.?;

View File

@@ -338,6 +338,8 @@ pub fn restoreOriginalProxy(self: *Client) !void {
// Enable TLS verification on all connections.
pub fn enableTlsVerify(self: *const Client) !void {
try self.ensureNoActiveConnection();
for (self.handles.handles) |*h| {
const easy = h.conn.easy;
@@ -353,6 +355,8 @@ pub fn enableTlsVerify(self: *const Client) !void {
// Disable TLS verification on all connections.
pub fn disableTlsVerify(self: *const Client) !void {
try self.ensureNoActiveConnection();
for (self.handles.handles) |*h| {
const easy = h.conn.easy;

View File

@@ -373,11 +373,7 @@ const Command = struct {
\\ Defaults to
++ (if (builtin.mode == .Debug) " pretty." else " logfmt.") ++
\\
\\--log_filter_scopes
\\ Filter out too verbose logs per scope:
\\ http, unknown_prop, script_event, ...
\\
\\--user_agent_suffix
\\ --user_agent_suffix
\\ Suffix to append to the Lightpanda/X.Y User-Agent
\\
;

View File

@@ -168,35 +168,35 @@
<script id=dimensions>
const para = document.getElementById('para');
testing.expectEqual(5, para.clientWidth);
testing.expectEqual(5, para.clientHeight);
testing.expectEqual(1, para.clientWidth);
testing.expectEqual(1, para.clientHeight);
let r1 = document.getElementById('para').getBoundingClientRect();
testing.expectEqual(0, r1.x);
testing.expectEqual(0, r1.y);
testing.expectEqual(5, r1.width);
testing.expectEqual(5, r1.height);
// let r1 = document.getElementById('para').getBoundingClientRect();
// testing.expectEqual(0, r1.x);
// testing.expectEqual(0, r1.y);
// testing.expectEqual(1, r1.width);
// testing.expectEqual(2, r1.height);
let r2 = document.getElementById('content').getBoundingClientRect();
testing.expectEqual(5, r2.x);
testing.expectEqual(0, r2.y);
testing.expectEqual(5, r2.width);
testing.expectEqual(5, r2.height);
// let r2 = document.getElementById('content').getBoundingClientRect();
// testing.expectEqual(1, r2.x);
// testing.expectEqual(0, r2.y);
// testing.expectEqual(1, r2.width);
// testing.expectEqual(1, r2.height);
let r3 = document.getElementById('para').getBoundingClientRect();
testing.expectEqual(0, r3.x);
testing.expectEqual(0, r3.y);
testing.expectEqual(5, r3.width);
testing.expectEqual(5, r3.height);
// let r3 = document.getElementById('para').getBoundingClientRect();
// testing.expectEqual(0, r3.x);
// testing.expectEqual(0, r3.y);
// testing.expectEqual(1, r3.width);
// testing.expectEqual(1, r3.height);
testing.expectEqual(10, para.clientWidth);
testing.expectEqual(5, para.clientHeight);
// testing.expectEqual(1, para.clientWidth);
// testing.expectEqual(1, para.clientHeight);
let r4 = document.createElement('div').getBoundingClientRect();
testing.expectEqual(0, r4.x);
testing.expectEqual(0, r4.y);
testing.expectEqual(0, r4.width);
testing.expectEqual(0, r4.height);
// let r4 = document.createElement('div').getBoundingClientRect();
// testing.expectEqual(0, r4.x);
// testing.expectEqual(0, r4.y);
// testing.expectEqual(0, r4.width);
// testing.expectEqual(0, r4.height);
</script>
<script id=matches>

View File

@@ -122,13 +122,13 @@
testing.expectEqual(1, entry.intersectionRatio);
testing.expectEqual(0, entry.intersectionRect.x);
testing.expectEqual(0, entry.intersectionRect.y);
testing.expectEqual(5, entry.intersectionRect.width);
testing.expectEqual(5, entry.intersectionRect.height);
testing.expectEqual(1, entry.intersectionRect.width);
testing.expectEqual(1, entry.intersectionRect.height);
testing.expectEqual(true, entry.isIntersecting);
testing.expectEqual(0, entry.rootBounds.x);
testing.expectEqual(0, entry.rootBounds.y);
testing.expectEqual(5, entry.rootBounds.width);
testing.expectEqual(5, entry.rootBounds.height);
testing.expectEqual(1, entry.rootBounds.width);
testing.expectEqual(1, entry.rootBounds.height);
testing.expectEqual('[object HTMLDivElement]', entry.target.toString());
});
}

View File

@@ -46,15 +46,15 @@
testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie);
// Return null since we only return elements when they have previously been localized
testing.expectEqual(null, document.elementFromPoint(2.5, 2.5));
testing.expectEqual([], document.elementsFromPoint(2.5, 2.5));
testing.expectEqual(null, document.elementFromPoint(0.5, 0.5));
testing.expectEqual([], document.elementsFromPoint(0.5, 0.5));
let div1 = document.createElement('div');
document.body.appendChild(div1);
div1.getClientRects(); // clal this to position it
testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(2.5, 2.5).toString());
testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(0.5, 0.5).toString());
let elems = document.elementsFromPoint(2.5, 2.5);
let elems = document.elementsFromPoint(0.5, 0.5);
testing.expectEqual(3, elems.length);
testing.expectEqual('[object HTMLDivElement]', elems[0].toString());
testing.expectEqual('[object HTMLBodyElement]', elems[1].toString());
@@ -66,11 +66,11 @@
// Note this will be placed after the div of previous test
a.getClientRects();
let a_again = document.elementFromPoint(7.5, 0.5);
let a_again = document.elementFromPoint(1.5, 0.5);
testing.expectEqual('[object HTMLAnchorElement]', a_again.toString());
testing.expectEqual('https://lightpanda.io', a_again.href);
let a_agains = document.elementsFromPoint(7.5, 0.5);
let a_agains = document.elementsFromPoint(1.5, 0.5);
testing.expectEqual('https://lightpanda.io', a_agains[0].href);

View File

@@ -16,8 +16,8 @@
testing.expectEqual('https://lightpanda.io', link.origin);
link.host = 'lightpanda.io:443';
testing.expectEqual('lightpanda.io', link.host);
testing.expectEqual('', link.port);
testing.expectEqual('lightpanda.io:443', link.host);
testing.expectEqual('443', link.port);
testing.expectEqual('lightpanda.io', link.hostname);
link.host = 'lightpanda.io';
@@ -42,9 +42,9 @@
testing.expectEqual('', link.port);
link.port = '443';
testing.expectEqual('foo.bar', link.host);
testing.expectEqual('foo.bar:443', link.host);
testing.expectEqual('foo.bar', link.hostname);
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
testing.expectEqual('https://foo.bar:443/?q=bar#frag', link.href);
link.port = null;
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);

View File

@@ -64,23 +64,6 @@
testing.expectEqual(null, url.searchParams.get('a'));
</script>
<script id=searchParamsSetHref>
url = new URL("https://foo.bar");
const searchParams = url.searchParams;
// SearchParams should be empty.
testing.expectEqual(0, searchParams.size);
url.href = "https://lightpanda.io?over=9000&light=panda";
// It won't hurt to check href and host too.
testing.expectEqual("https://lightpanda.io/?over=9000&light=panda", url.href);
testing.expectEqual("lightpanda.io", url.host);
// SearchParams should be updated too when URL is set.
testing.expectEqual(2, searchParams.size);
testing.expectEqual("9000", searchParams.get("over"));
testing.expectEqual("panda", searchParams.get("light"));
</script>
<script id=base>
url = new URL('over?9000', 'https://lightpanda.io');
testing.expectEqual("https://lightpanda.io/over?9000", url.href);
@@ -95,15 +78,6 @@
</script>
<script id=invalidUrl>
testing.expectError("Error: Invalid", () => {
_ = new URL("://foo.bar/path?query#fragment");
});
</script>
<script id=URL.canParse>
testing.expectEqual(true, URL.canParse("https://lightpanda.io"));
testing.expectEqual(false, URL.canParse("://lightpanda.io"));
testing.expectEqual(true, URL.canParse("/home", "https://lightpanda.io"));
testing.expectEqual(false, URL.canParse("lightpanda.io", "https"));
let u = new URL("://foo.bar/path?query#fragment");
testing.expectEqual(":", u.protocol);
</script>

View File

@@ -25,9 +25,9 @@
</script>
<script id=dimensions>
testing.expectEqual(5, innerHeight);
// Width is 5 even if there are no elements
testing.expectEqual(5, innerWidth);
testing.expectEqual(1, innerHeight);
// Width is 1 even if there are no elements
testing.expectEqual(1, innerWidth);
let div1 = document.createElement('div');
document.body.appendChild(div1);
@@ -37,8 +37,8 @@
document.body.appendChild(div2);
div2.getClientRects();
testing.expectEqual(5, innerHeight);
testing.expectEqual(10, innerWidth);
testing.expectEqual(1, innerHeight);
testing.expectEqual(2, innerWidth);
</script>
<script id=setTimeout>

View File

@@ -75,6 +75,10 @@ pub const URL = struct {
return writer.writeAll(self.raw);
}
pub fn toWebApi(self: *const URL, allocator: Allocator) !WebApiURL {
return WebApiURL.init(allocator, self.uri);
}
/// Properly stitches two URL fragments together.
///
/// For URLs with a path, it will replace the last entry with the src.

176
vendor/ada/root.zig vendored
View File

@@ -1,176 +0,0 @@
//! Wrappers for ada URL parser.
//! https://github.com/ada-url/ada
const c = @cImport({
@cInclude("ada_c.h");
});
pub const URLComponents = c.ada_url_components;
pub const URLOmitted = c.ada_url_omitted;
pub const String = c.ada_string;
pub const OwnedString = c.ada_owned_string;
/// Pointer types.
pub const URL = c.ada_url;
pub const URLSearchParams = c.ada_url_search_params;
pub const ParseError = error{Invalid};
pub fn parse(input: []const u8) ParseError!URL {
const url = c.ada_parse(input.ptr, input.len);
if (!c.ada_is_valid(url)) {
return error.Invalid;
}
return url;
}
pub fn parseWithBase(input: []const u8, base: []const u8) ParseError!URL {
const url = c.ada_parse_with_base(input.ptr, input.len, base.ptr, base.len);
if (!c.ada_is_valid(url)) {
return error.Invalid;
}
return url;
}
pub inline fn canParse(input: []const u8) bool {
return c.ada_can_parse(input.ptr, input.len);
}
pub inline fn canParseWithBase(input: []const u8, base: []const u8) bool {
return c.ada_can_parse_with_base(input.ptr, input.len, base.ptr, base.len);
}
pub inline fn getComponents(url: URL) *const URLComponents {
return c.ada_get_components(url);
}
pub inline fn free(url: URL) void {
return c.ada_free(url);
}
pub inline fn freeOwnedString(owned: OwnedString) void {
return c.ada_free_owned_string(owned);
}
/// Returns true if given URL is valid.
pub inline fn isValid(url: URL) bool {
return c.ada_is_valid(url);
}
/// Creates a new `URL` from given `URL`.
pub inline fn copy(url: URL) URL {
return c.ada_copy(url);
}
/// Contrary to other getters, this heap allocates.
pub inline fn getOriginNullable(url: URL) OwnedString {
return c.ada_get_origin(url);
}
pub inline fn getHrefNullable(url: URL) String {
return c.ada_get_href(url);
}
pub inline fn getUsernameNullable(url: URL) String {
return c.ada_get_username(url);
}
pub inline fn getPasswordNullable(url: URL) String {
return c.ada_get_password(url);
}
pub inline fn getSearchNullable(url: URL) String {
return c.ada_get_search(url);
}
pub inline fn getPortNullable(url: URL) String {
return c.ada_get_port(url);
}
pub inline fn getHashNullable(url: URL) String {
return c.ada_get_hash(url);
}
pub inline fn getHostNullable(url: URL) String {
return c.ada_get_host(url);
}
pub inline fn getHostnameNullable(url: URL) String {
return c.ada_get_hostname(url);
}
pub inline fn getPathnameNullable(url: URL) String {
return c.ada_get_pathname(url);
}
pub inline fn getProtocolNullable(url: URL) String {
return c.ada_get_protocol(url);
}
pub inline fn setHref(url: URL, input: []const u8) bool {
return c.ada_set_href(url, input.ptr, input.len);
}
pub inline fn setHost(url: URL, input: []const u8) bool {
return c.ada_set_host(url, input.ptr, input.len);
}
pub inline fn setHostname(url: URL, input: []const u8) bool {
return c.ada_set_hostname(url, input.ptr, input.len);
}
pub inline fn setProtocol(url: URL, input: []const u8) bool {
return c.ada_set_protocol(url, input.ptr, input.len);
}
pub inline fn setUsername(url: URL, input: []const u8) bool {
return c.ada_set_username(url, input.ptr, input.len);
}
pub inline fn setPassword(url: URL, input: []const u8) bool {
return c.ada_set_password(url, input.ptr, input.len);
}
pub inline fn setPort(url: URL, input: []const u8) bool {
return c.ada_set_port(url, input.ptr, input.len);
}
pub inline fn setPathname(url: URL, input: []const u8) bool {
return c.ada_set_pathname(url, input.ptr, input.len);
}
pub inline fn setSearch(url: URL, input: []const u8) void {
return c.ada_set_search(url, input.ptr, input.len);
}
pub inline fn setHash(url: URL, input: []const u8) void {
return c.ada_set_hash(url, input.ptr, input.len);
}
pub inline fn clearHash(url: URL) void {
return c.ada_clear_hash(url);
}
pub inline fn clearSearch(url: URL) void {
return c.ada_clear_search(url);
}
pub inline fn clearPort(url: URL) void {
return c.ada_clear_port(url);
}
pub const Scheme = struct {
pub const http: u8 = 0;
pub const not_special: u8 = 1;
pub const https: u8 = 2;
pub const ws: u8 = 3;
pub const ftp: u8 = 4;
pub const wss: u8 = 5;
pub const file: u8 = 6;
};
/// Returns one of the constants defined in `Scheme`.
pub inline fn getSchemeType(url: URL) u8 {
return c.ada_get_scheme_type(url);
}