refactor HTMLAnchorElement

Prefer new URL implementation with separate store for object data.
This commit is contained in:
Halil Durak
2025-10-17 19:39:10 +03:00
parent 8e7d8225ba
commit 146b56c8c0
4 changed files with 249 additions and 105 deletions

View File

@@ -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());
}
};

View File

@@ -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,

View File

@@ -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);

12
vendor/ada/root.zig vendored
View File

@@ -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);