diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 38d39fbd..6dd18601 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -221,9 +221,29 @@ pub const HTMLAnchorElement = struct { return parser.anchorGetHref(self); } - pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void { + pub fn set_href(self: *parser.Anchor, href: []const u8, page: *Page) !void { const full = try urlStitch(page.call_arena, href, page.url.getHref(), .{}); - return parser.anchorSetHref(self, full); + + // Get the stored internal URL if we had one. + if (page.getObjectData(self)) |internal_url| { + const u = NativeURL.fromInternal(internal_url); + // Reparse with the new href. + _ = try u.reparse(full); + errdefer u.deinit(); + + // TODO: Remove the entry from the map on an error situation. + + return parser.anchorSetHref(self, u.getHref()); + } + + // We don't have internal URL stored in object_data yet. + // Create one for this anchor element. + const u = try NativeURL.parse(full, null); + errdefer u.deinit(); + // Save to map. + try page.putObjectData(self, u.internal.?); + + return parser.anchorSetHref(self, u.getHref()); } pub fn get_hreflang(self: *parser.Anchor) ![]const u8 { @@ -258,170 +278,188 @@ pub const HTMLAnchorElement = struct { return try parser.nodeSetTextContent(parser.anchorToNode(self), v); } - fn url(self: *parser.Anchor, page: *Page) !URL { - // Although the URL.constructor union accepts an .{.element = X}, we - // can't use this here because the behavior is different. - // URL.constructor(document.createElement('a') - // should fail (a.href isn't a valid URL) - // But - // document.createElement('a').host - // should not fail, it should return an empty string - if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| { - return URL.constructor(.{ .string = href }, null, page); // TODO inject base url + fn getHref(self: *parser.Anchor) !?[]const u8 { + return parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href"); + } + + /// Returns the URL associated with given anchor element. + /// Creates a new URL object if not created before. + fn getURL(self: *parser.Anchor, page: *Page) !NativeURL { + if (page.getObjectData(self)) |internal_url| { + return NativeURL.fromInternal(internal_url); } - return .empty; + + // Try to get href string. + const maybe_anchor_href = try getHref(self); + if (maybe_anchor_href) |anchor_href| { + // Allocate a URL for this anchor element. + const u = try NativeURL.parse(anchor_href, null); + // Save in map. + try page.putObjectData(self, u.internal.?); + + return u; + } + + // No anchor href string found; let's just return an error. + return error.HrefAttributeNotGiven; } // TODO return a disposable string pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_origin(page); + const u = getURL(self, page) catch return ""; + // Though we store the URL in object data map, we still have to allocate + // for origin string sadly. + return u.getOrigin(page.arena); } // TODO return a disposable string pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_protocol(); + const u = getURL(self, page) catch return ""; + return u.getProtocol(); } pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void { - const arena = page.arena; - _ = arena; - var u = try url(self, page); - - u.set_protocol(v); - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + const u = try getURL(self, page); + try u.setProtocol(v); + return parser.anchorSetHref(self, u.getHref()); } + const NativeURL = @import("../../url.zig").URL; + + // TODO: Return a disposable string. pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_host(); + const u = getURL(self, page) catch return ""; + return u.host(); } - pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void { - // search : separator - var p: ?[]const u8 = 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); - p = v[i + 1 ..]; - break; + pub fn set_host(self: *parser.Anchor, host_str: []const u8, page: *Page) !void { + const u = blk: { + if (page.getObjectData(self)) |internal_url| { + break :blk NativeURL.fromInternal(internal_url); } - } - var u = try url(self, page); + const maybe_anchor_href = try getHref(self); + if (maybe_anchor_href) |anchor_href| { + const new_u = try NativeURL.parse(anchor_href, null); + try page.putObjectData(self, new_u.internal.?); + break :blk new_u; + } - if (p) |port| { - u.set_host(h); - u.set_port(port); - } else { - u.set_host(v); - } + // Last resort; try to create URL object out of host_str. + const new_u = try NativeURL.parse(host_str, null); + // We can just return here since host is updated. + return page.putObjectData(self, new_u.internal.?); + }; - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + try u.setHost(host_str); + return parser.anchorSetHref(self, u.getHref()); } - pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_hostname(); - } + //pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 { + // const maybe_href_str = try getAnchorHref(self); + // const href_str = maybe_href_str orelse return ""; + // + // const u = try NativeURL.parse(href_str, null); + // defer u.deinit(); + // + // return page.arena.dupe(u8, u.getHostname()); + //} - pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void { - var u = try url(self, page); - u.set_host(v); - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); - } + //pub fn set_hostname(self: *parser.Anchor, v: []const u8) !void { + // const maybe_href_str = try getAnchorHref(self); + // + // if (maybe_href_str) |href_str| { + // const u = try NativeURL.parse(href_str, null); + // defer u.deinit(); + // + // try u.setHostname(v); + // + // return parser.anchorSetHref(self, u.getHref()); + // } + // + // // No href string there; use the given value as href. + // const u = try NativeURL.parse(v, null); + // defer u.deinit(); + // + // return parser.anchorSetHref(self, u.getHref()); + //} // TODO return a disposable string pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_port(); + const u = getURL(self, page) catch return ""; + return u.getPort(); } - pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { - var u = try url(self, page); + pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void { + // TODO: Check for valid port (u16 integer). + if (maybe_port) |port| { + const u = try getURL(self, page); + try u.setPort(port); - if (v != null and v.?.len > 0) { - u.set_host(v.?); + return parser.anchorSetHref(self, u.getHref()); } - - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); } // TODO return a disposable string pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_username(); + const u = try getURL(self, page); + return u.getUsername() orelse ""; } - pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { - if (v) |username| { - var u = try url(self, page); - u.set_username(username); - - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void { + if (maybe_username) |username| { + const u = try getURL(self, page); + try u.setUsername(username); + try parser.anchorSetHref(self, u.getHref()); } } pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_password(); + const u = try getURL(self, page); + return u.getPassword() orelse ""; } - pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { - if (v) |password| { - var u = try url(self, page); - u.set_password(password); - - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void { + if (maybe_password) |password| { + const u = try getURL(self, page); + try u.setPassword(password); + try parser.anchorSetHref(self, u.getHref()); } } // TODO return a disposable string pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_pathname(); + const u = try getURL(self, page); + return u.getPath(); } - pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void { - var u = try url(self, page); - u.set_pathname(v); - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void { + const u = try getURL(self, page); + try u.setPath(pathname); + return parser.anchorSetHref(self, u.getHref()); } pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_search(page); + const u = try getURL(self, page); + return u.getSearch() orelse ""; } - pub fn set_search(self: *parser.Anchor, v: []const u8, page: *Page) !void { - var u = try url(self, page); - try u.set_search(v, page); - - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + pub fn set_search(self: *parser.Anchor, search: []const u8, page: *Page) !void { + const u = try getURL(self, page); + u.setSearch(search); + return parser.anchorSetHref(self, u.getHref()); } // TODO return a disposable string pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_hash(); + const u = try getURL(self, page); + return u.getHash() orelse ""; } - pub fn set_hash(self: *parser.Anchor, v: []const u8, page: *Page) !void { - var u = try url(self, page); - u.set_hash(v); - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + pub fn set_hash(self: *parser.Anchor, hash: []const u8, page: *Page) !void { + const u = try getURL(self, page); + u.setHash(hash); + return parser.anchorSetHref(self, u.getHref()); } }; diff --git a/src/browser/page.zig b/src/browser/page.zig index 718ed756..a553ae11 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -84,6 +84,11 @@ pub const Page = struct { polyfill_loader: polyfill.Loader = .{}, + /// KV map for various object data; use pointers as unsigned integer keys + /// and store any `*anyopaque` as values. If a key or value will be + /// deinitialized (freed), it should be removed from the map too. + object_data: ObjectDataMap = .{}, + scheduler: Scheduler, http_client: *Http.Client, script_manager: ScriptManager, @@ -122,6 +127,21 @@ pub const Page = struct { complete, }; + const ObjectDataMap = std.HashMapUnmanaged( + usize, + *anyopaque, + struct { + pub fn hash(_: @This(), key: usize) usize { + return key; + } + + pub fn eql(_: @This(), a: usize, b: usize) bool { + return a == b; + } + }, + std.hash_map.default_max_load_percentage, + ); + pub fn init(self: *Page, arena: Allocator, call_arena: Allocator, session: *Session) !void { const browser = session.browser; const script_manager = ScriptManager.init(browser, self); @@ -160,6 +180,7 @@ pub const Page = struct { self.http_client.abort(); self.script_manager.deinit(); self.url.deinit(); + self.object_data.deinit(self.arena); } fn reset(self: *Page) !void { @@ -170,6 +191,12 @@ pub const Page = struct { self.http_client.abort(); self.script_manager.reset(); + _ = try self.url.reparse("about:blank"); + errdefer self.url.deinit(); + + self.object_data.deinit(self.arena); + self.object_data = .{}; + self.load_state = .parsing; self.mode = .{ .pre = {} }; _ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); @@ -200,6 +227,21 @@ pub const Page = struct { }.runMessageLoop, 5, .{ .name = "page.messageLoop" }); } + /// Returns the object data by given key. + /// `key` must be a pointer type. + /// Type of value is unknown to map; so the caller must do the type casting. + pub fn getObjectData(self: *Page, key: anytype) ?*anyopaque { + std.debug.assert(@typeInfo(@TypeOf(key)) == .pointer); + return self.object_data.get(@intFromPtr(key)); + } + + /// Puts the object data by given key. + /// `key` must be a pointer type. + pub fn putObjectData(self: *Page, key: anytype, value: *anyopaque) Allocator.Error!void { + std.debug.assert(@typeInfo(@TypeOf(key)) == .pointer); + return self.object_data.put(self.arena, @intFromPtr(key), value); + } + pub const DumpOpts = struct { // set to include element shadowroots in the dump page: ?*const Page = null, diff --git a/src/url.zig b/src/url.zig index 42d083ab..8266af1d 100644 --- a/src/url.zig +++ b/src/url.zig @@ -44,6 +44,11 @@ pub const URL = struct { return self; } + /// Forms a `URL` from given `internal`. Memory is not copied. + pub fn fromInternal(internal: ada.URL) URL { + return .{ .internal = internal }; + } + /// Deinitializes internal url. pub fn deinit(self: URL) void { std.debug.assert(self.internal != null); @@ -100,6 +105,28 @@ pub const URL = struct { if (!is_set) return error.InvalidHostname; } + pub fn getUsername(self: URL) ?[]const u8 { + const username = ada.getUsernameNullable(self.internal); + if (username.data == null) return null; + return username.data[0..username.length]; + } + + pub fn setUsername(self: URL, username: []const u8) error{InvalidUsername}!void { + const is_set = ada.setUsername(self.internal, username); + if (!is_set) return error.InvalidUsername; + } + + pub fn getPassword(self: URL) ?[]const u8 { + const password = ada.getPasswordNullable(self.internal); + if (password.data == null) return null; + return password.data[0..password.length]; + } + + pub fn setPassword(self: URL, password: []const u8) error{InvalidPassword}!void { + const is_set = ada.setPassword(self.internal, password); + if (!is_set) return error.InvalidPassword; + } + pub fn getFragment(self: URL) ?[]const u8 { // Ada calls it "hash" instead of "fragment". const hash = ada.getHashNullable(self.internal); @@ -108,6 +135,26 @@ pub const URL = struct { return hash.data[0..hash.length]; } + pub fn getSearch(self: URL) ?[]const u8 { + const search = ada.getSearchNullable(self.internal); + if (search.data == null) return null; + return search.data[0..search.length]; + } + + pub fn setSearch(self: URL, search: []const u8) void { + return ada.setSearch(self.internal, search); + } + + pub fn getHash(self: URL) ?[]const u8 { + const hash = ada.getHashNullable(self.internal); + if (hash.data == null) return null; + return hash.data[0..hash.length]; + } + + pub fn setHash(self: URL, hash: []const u8) void { + return ada.setHash(self.internal, hash); + } + pub fn getProtocol(self: URL) []const u8 { return ada.getProtocol(self.internal); } @@ -135,6 +182,11 @@ pub const URL = struct { return pathname.data[0..pathname.length]; } + pub fn setPath(self: URL, path: []const u8) error{InvalidPath}!void { + const is_set = ada.setPathname(self.internal, path); + if (!is_set) return error.InvalidPath; + } + /// Returns true if the URL's protocol is secure. pub fn isSecure(self: URL) bool { const scheme = ada.getSchemeType(self.internal); diff --git a/vendor/ada/root.zig b/vendor/ada/root.zig index 1af8e823..1a54b40e 100644 --- a/vendor/ada/root.zig +++ b/vendor/ada/root.zig @@ -71,12 +71,24 @@ 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); +} + /// Can return an empty string. pub inline fn getUsername(url: URL) []const u8 { const username = c.ada_get_username(url); return username.data[0..username.length]; } +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); +} + /// Can return an empty string. pub inline fn getPassword(url: URL) []const u8 { const password = c.ada_get_password(url);