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); 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(), .{}); 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 { 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); return try parser.nodeSetTextContent(parser.anchorToNode(self), v);
} }
fn url(self: *parser.Anchor, page: *Page) !URL { fn getHref(self: *parser.Anchor) !?[]const u8 {
// Although the URL.constructor union accepts an .{.element = X}, we return parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href");
// can't use this here because the behavior is different. }
// URL.constructor(document.createElement('a')
// should fail (a.href isn't a valid URL) /// Returns the URL associated with given anchor element.
// But /// Creates a new URL object if not created before.
// document.createElement('a').host fn getURL(self: *parser.Anchor, page: *Page) !NativeURL {
// should not fail, it should return an empty string if (page.getObjectData(self)) |internal_url| {
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| { return NativeURL.fromInternal(internal_url);
return URL.constructor(.{ .string = href }, null, page); // TODO inject base 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 // TODO return a disposable string
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = getURL(self, page) catch return "";
return u.get_origin(page); // 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 // TODO return a disposable string
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = getURL(self, page) catch return "";
return u.get_protocol(); return u.getProtocol();
} }
pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena; const u = try getURL(self, page);
_ = arena; try u.setProtocol(v);
var u = try url(self, page); return parser.anchorSetHref(self, u.getHref());
u.set_protocol(v);
const href = try u.get_href(page);
try parser.anchorSetHref(self, href);
} }
const NativeURL = @import("../../url.zig").URL;
// TODO: Return a disposable string.
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = getURL(self, page) catch return "";
return u.get_host(); return u.host();
} }
pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_host(self: *parser.Anchor, host_str: []const u8, page: *Page) !void {
// search : separator const u = blk: {
var p: ?[]const u8 = null; if (page.getObjectData(self)) |internal_url| {
var h: []const u8 = undefined; break :blk NativeURL.fromInternal(internal_url);
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;
} }
}
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| { // Last resort; try to create URL object out of host_str.
u.set_host(h); const new_u = try NativeURL.parse(host_str, null);
u.set_port(port); // We can just return here since host is updated.
} else { return page.putObjectData(self, new_u.internal.?);
u.set_host(v); };
}
const href = try u.get_href(page); try u.setHost(host_str);
try parser.anchorSetHref(self, href); return parser.anchorSetHref(self, u.getHref());
} }
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 { //pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); // const maybe_href_str = try getAnchorHref(self);
return u.get_hostname(); // 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 { //pub fn set_hostname(self: *parser.Anchor, v: []const u8) !void {
var u = try url(self, page); // const maybe_href_str = try getAnchorHref(self);
u.set_host(v); //
const href = try u.get_href(page); // if (maybe_href_str) |href_str| {
try parser.anchorSetHref(self, href); // 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 // TODO return a disposable string
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = getURL(self, page) catch return "";
return u.get_port(); return u.getPort();
} }
pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void {
var u = try url(self, page); // 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) { return parser.anchorSetHref(self, u.getHref());
u.set_host(v.?);
} }
const href = try u.get_href(page);
try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = try getURL(self, page);
return u.get_username(); return u.getUsername() orelse "";
} }
pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void {
if (v) |username| { if (maybe_username) |username| {
var u = try url(self, page); const u = try getURL(self, page);
u.set_username(username); try u.setUsername(username);
try parser.anchorSetHref(self, u.getHref());
const href = try u.get_href(page);
try parser.anchorSetHref(self, href);
} }
} }
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = try getURL(self, page);
return u.get_password(); return u.getPassword() orelse "";
} }
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
if (v) |password| { if (maybe_password) |password| {
var u = try url(self, page); const u = try getURL(self, page);
u.set_password(password); try u.setPassword(password);
try parser.anchorSetHref(self, u.getHref());
const href = try u.get_href(page);
try parser.anchorSetHref(self, href);
} }
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = try getURL(self, page);
return u.get_pathname(); return u.getPath();
} }
pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void {
var u = try url(self, page); const u = try getURL(self, page);
u.set_pathname(v); try u.setPath(pathname);
const href = try u.get_href(page); return parser.anchorSetHref(self, u.getHref());
try parser.anchorSetHref(self, href);
} }
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = try getURL(self, page);
return u.get_search(page); return u.getSearch() orelse "";
} }
pub fn set_search(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_search(self: *parser.Anchor, search: []const u8, page: *Page) !void {
var u = try url(self, page); const u = try getURL(self, page);
try u.set_search(v, page); u.setSearch(search);
return parser.anchorSetHref(self, u.getHref());
const href = try u.get_href(page);
try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = try getURL(self, page);
return u.get_hash(); return u.getHash() orelse "";
} }
pub fn set_hash(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_hash(self: *parser.Anchor, hash: []const u8, page: *Page) !void {
var u = try url(self, page); const u = try getURL(self, page);
u.set_hash(v); u.setHash(hash);
const href = try u.get_href(page); return parser.anchorSetHref(self, u.getHref());
try parser.anchorSetHref(self, href);
} }
}; };

View File

@@ -84,6 +84,11 @@ pub const Page = struct {
polyfill_loader: polyfill.Loader = .{}, 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, scheduler: Scheduler,
http_client: *Http.Client, http_client: *Http.Client,
script_manager: ScriptManager, script_manager: ScriptManager,
@@ -122,6 +127,21 @@ pub const Page = struct {
complete, 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 { pub fn init(self: *Page, arena: Allocator, call_arena: Allocator, session: *Session) !void {
const browser = session.browser; const browser = session.browser;
const script_manager = ScriptManager.init(browser, self); const script_manager = ScriptManager.init(browser, self);
@@ -160,6 +180,7 @@ pub const Page = struct {
self.http_client.abort(); self.http_client.abort();
self.script_manager.deinit(); self.script_manager.deinit();
self.url.deinit(); self.url.deinit();
self.object_data.deinit(self.arena);
} }
fn reset(self: *Page) !void { fn reset(self: *Page) !void {
@@ -170,6 +191,12 @@ pub const Page = struct {
self.http_client.abort(); self.http_client.abort();
self.script_manager.reset(); 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.load_state = .parsing;
self.mode = .{ .pre = {} }; self.mode = .{ .pre = {} };
_ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); _ = 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" }); }.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 { pub const DumpOpts = struct {
// set to include element shadowroots in the dump // set to include element shadowroots in the dump
page: ?*const Page = null, page: ?*const Page = null,

View File

@@ -44,6 +44,11 @@ pub const URL = struct {
return self; 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. /// Deinitializes internal url.
pub fn deinit(self: URL) void { pub fn deinit(self: URL) void {
std.debug.assert(self.internal != null); std.debug.assert(self.internal != null);
@@ -100,6 +105,28 @@ pub const URL = struct {
if (!is_set) return error.InvalidHostname; 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 { pub fn getFragment(self: URL) ?[]const u8 {
// Ada calls it "hash" instead of "fragment". // Ada calls it "hash" instead of "fragment".
const hash = ada.getHashNullable(self.internal); const hash = ada.getHashNullable(self.internal);
@@ -108,6 +135,26 @@ pub const URL = struct {
return hash.data[0..hash.length]; 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 { pub fn getProtocol(self: URL) []const u8 {
return ada.getProtocol(self.internal); return ada.getProtocol(self.internal);
} }
@@ -135,6 +182,11 @@ pub const URL = struct {
return pathname.data[0..pathname.length]; 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. /// Returns true if the URL's protocol is secure.
pub fn isSecure(self: URL) bool { pub fn isSecure(self: URL) bool {
const scheme = ada.getSchemeType(self.internal); 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); return c.ada_get_href(url);
} }
pub inline fn getUsernameNullable(url: URL) String {
return c.ada_get_username(url);
}
/// Can return an empty string. /// Can return an empty string.
pub inline fn getUsername(url: URL) []const u8 { pub inline fn getUsername(url: URL) []const u8 {
const username = c.ada_get_username(url); const username = c.ada_get_username(url);
return username.data[0..username.length]; 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. /// Can return an empty string.
pub inline fn getPassword(url: URL) []const u8 { pub inline fn getPassword(url: URL) []const u8 {
const password = c.ada_get_password(url); const password = c.ada_get_password(url);