From 83b552780e6ccee4d445d1572c8f247d4b701d99 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 17 Nov 2025 23:19:32 +0800 Subject: [PATCH] enhance Anchor API --- src/browser/tests/element/html/anchor.html | 59 ++++++ src/browser/webapi/element/html/Anchor.zig | 201 ++++++++++++++++++++- 2 files changed, 256 insertions(+), 4 deletions(-) diff --git a/src/browser/tests/element/html/anchor.html b/src/browser/tests/element/html/anchor.html index 43440b4e..a1402688 100644 --- a/src/browser/tests/element/html/anchor.html +++ b/src/browser/tests/element/html/anchor.html @@ -11,3 +11,62 @@ testing.expectEqual('http://127.0.0.1:9582/hello/world/anchor2.html', $('#a2').href) testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href) + +OK + + diff --git a/src/browser/webapi/element/html/Anchor.zig b/src/browser/webapi/element/html/Anchor.zig index 5a0f6b60..8821ec61 100644 --- a/src/browser/webapi/element/html/Anchor.zig +++ b/src/browser/webapi/element/html/Anchor.zig @@ -16,10 +16,11 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); const js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); -const URL = @import("../../URL.zig"); +const URL = @import("../../../URL.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -35,11 +36,194 @@ pub fn asNode(self: *Anchor) *Node { } pub fn getHref(self: *Anchor, page: *Page) ![]const u8 { - const el = self.asElement(); - const href = el.getAttributeSafe("href"); + const element = self.asElement(); + const href = element.getAttributeSafe("href") orelse ""; + if (href.len == 0) { + return page.url; + } + + const first = href[0]; + if (first == '#' or first == '?' or first == '/' or std.mem.startsWith(u8, href, "../") or std.mem.startsWith(u8, href, "./")) { + return URL.resolve(page.call_arena, page.url, href, .{}); + } + + return href; +} + +pub fn setHref(self: *Anchor, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("href", value, page); +} + +pub fn getTarget(self: *Anchor) []const u8 { + return self.asElement().getAttributeSafe("target") orelse ""; +} + +pub fn setTarget(self: *Anchor, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("target", value, page); +} + +pub fn getOrigin(self: *Anchor, page: *Page) ![]const u8 { + const href = try getResolvedHref(self, page); + return (try URL.getOrigin(page.call_arena, href)) orelse "null"; +} + +pub fn getHost(self: *Anchor, page: *Page) ![]const u8 { + const href = try getResolvedHref(self, page); + return URL.getHost(href); +} + +pub fn setHost(self: *Anchor, value: []const u8, page: *Page) !void { + const href = try getResolvedHref(self, page); + const protocol = URL.getProtocol(href); + const pathname = URL.getPathname(href); + const search = URL.getSearch(href); + const hash = URL.getHash(href); + + // Check if the host includes a port + const colon_pos = std.mem.lastIndexOfScalar(u8, value, ':'); + const clean_host = if (colon_pos) |pos| blk: { + const port_str = value[pos + 1 ..]; + // Remove default ports + if (std.mem.eql(u8, protocol, "https:") and std.mem.eql(u8, port_str, "443")) { + break :blk value[0..pos]; + } + if (std.mem.eql(u8, protocol, "http:") and std.mem.eql(u8, port_str, "80")) { + break :blk value[0..pos]; + } + break :blk value; + } else value; + + const new_href = try buildUrl(page.call_arena, protocol, clean_host, pathname, search, hash); + try setHref(self, new_href, page); +} + +pub fn getHostname(self: *Anchor, page: *Page) ![]const u8 { + const href = try getResolvedHref(self, page); + return URL.getHostname(href); +} + +pub fn setHostname(self: *Anchor, value: []const u8, page: *Page) !void { + const href = try getResolvedHref(self, page); + const current_port = URL.getPort(href); + const new_host = if (current_port.len > 0) + try std.fmt.allocPrint(page.call_arena, "{s}:{s}", .{ value, current_port }) + else + value; + + try setHost(self, new_host, page); +} + +pub fn getPort(self: *Anchor, page: *Page) ![]const u8 { + const href = try getResolvedHref(self, page); + return URL.getPort(href); +} + +pub fn setPort(self: *Anchor, value: ?[]const u8, page: *Page) !void { + const href = try getResolvedHref(self, page); + const hostname = URL.getHostname(href); + const protocol = URL.getProtocol(href); + + // Handle null or default ports + const new_host = if (value) |port_str| blk: { + if (port_str.len == 0) { + break :blk hostname; + } + // Check if this is a default port for the protocol + if (std.mem.eql(u8, protocol, "https:") and std.mem.eql(u8, port_str, "443")) { + break :blk hostname; + } + if (std.mem.eql(u8, protocol, "http:") and std.mem.eql(u8, port_str, "80")) { + break :blk hostname; + } + break :blk try std.fmt.allocPrint(page.call_arena, "{s}:{s}", .{ hostname, port_str }); + } else hostname; + + try setHost(self, new_host, page); +} + +pub fn getSearch(self: *Anchor, page: *Page) ![]const u8 { + const href = try getResolvedHref(self, page); + return URL.getSearch(href); +} + +pub fn setSearch(self: *Anchor, value: []const u8, page: *Page) !void { + const href = try getResolvedHref(self, page); + const protocol = URL.getProtocol(href); + const host = URL.getHost(href); + const pathname = URL.getPathname(href); + const hash = URL.getHash(href); + + // Add ? prefix if not present and value is not empty + const search = if (value.len > 0 and value[0] != '?') + try std.fmt.allocPrint(page.call_arena, "?{s}", .{value}) + else + value; + + const new_href = try buildUrl(page.call_arena, protocol, host, pathname, search, hash); + try setHref(self, new_href, page); +} + +pub fn getHash(self: *Anchor, page: *Page) ![]const u8 { + const href = try getResolvedHref(self, page); + return URL.getHash(href); +} + +pub fn setHash(self: *Anchor, value: []const u8, page: *Page) !void { + const href = try getResolvedHref(self, page); + const protocol = URL.getProtocol(href); + const host = URL.getHost(href); + const pathname = URL.getPathname(href); + const search = URL.getSearch(href); + + // Add # prefix if not present and value is not empty + const hash = if (value.len > 0 and value[0] != '#') + try std.fmt.allocPrint(page.call_arena, "#{s}", .{value}) + else + value; + + const new_href = try buildUrl(page.call_arena, protocol, host, pathname, search, hash); + try setHref(self, new_href, page); +} + +pub fn getType(self: *Anchor) []const u8 { + return self.asElement().getAttributeSafe("type") orelse ""; +} + +pub fn setType(self: *Anchor, value: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe("type", value, page); +} + +pub fn getText(self: *Anchor, page: *Page) ![:0]const u8 { + return self.asNode().getTextContentAlloc(page.call_arena); +} + +pub fn setText(self: *Anchor, value: []const u8, page: *Page) !void { + try self.asNode().setTextContent(value, page); +} + +fn getResolvedHref(self: *Anchor, page: *Page) ![:0]const u8 { + const href = self.asElement().getAttributeSafe("href"); return URL.resolve(page.call_arena, page.url, href orelse "", .{}); } +// Helper function to build a new URL from components +fn buildUrl( + allocator: std.mem.Allocator, + protocol: []const u8, + host: []const u8, + pathname: []const u8, + search: []const u8, + hash: []const u8, +) ![:0]const u8 { + return std.fmt.allocPrintSentinel(allocator, "{s}//{s}{s}{s}{s}", .{ + protocol, + host, + pathname, + search, + hash, + }, 0); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Anchor); @@ -49,7 +233,16 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; - pub const href = bridge.accessor(Anchor.getHref, null, .{}); + pub const href = bridge.accessor(Anchor.getHref, Anchor.setHref, .{}); + pub const target = bridge.accessor(Anchor.getTarget, Anchor.setTarget, .{}); + pub const origin = bridge.accessor(Anchor.getOrigin, null, .{}); + pub const host = bridge.accessor(Anchor.getHost, Anchor.setHost, .{}); + pub const hostname = bridge.accessor(Anchor.getHostname, Anchor.setHostname, .{}); + pub const port = bridge.accessor(Anchor.getPort, Anchor.setPort, .{}); + pub const search = bridge.accessor(Anchor.getSearch, Anchor.setSearch, .{}); + pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{}); + pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{}); + pub const text = bridge.accessor(Anchor.getText, Anchor.setText, .{}); }; const testing = @import("../../../../testing.zig");