enhance Anchor API

This commit is contained in:
Karl Seguin
2025-11-17 23:19:32 +08:00
parent b8cc74f377
commit 83b552780e
2 changed files with 256 additions and 4 deletions

View File

@@ -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)
</script>
<a id=link href=foo>OK</a>
<script id=anchor>
let link = $('#link');
testing.expectEqual('', link.target);
link.target = '_blank';
testing.expectEqual('_blank', link.target);
link.target = '';
testing.expectEqual('foo', link.href);
link.href = 'https://lightpanda.io/';
testing.expectEqual('https://lightpanda.io/', link.href);
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', link.hostname);
link.host = 'lightpanda.io';
testing.expectEqual('lightpanda.io', link.host);
testing.expectEqual('', link.port);
testing.expectEqual('lightpanda.io', link.hostname);
testing.expectEqual('lightpanda.io', link.host);
testing.expectEqual('lightpanda.io', link.hostname);
link.hostname = 'foo.bar';
testing.expectEqual('https://foo.bar/', link.href);
testing.expectEqual('', link.search);
link.search = 'q=bar';
testing.expectEqual('?q=bar', link.search);
testing.expectEqual('https://foo.bar/?q=bar', link.href);
testing.expectEqual('', link.hash);
link.hash = 'frag';
testing.expectEqual('#frag', link.hash);
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
testing.expectEqual('', link.port);
link.port = '443';
testing.expectEqual('foo.bar', link.host);
testing.expectEqual('foo.bar', link.hostname);
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
link.port = null;
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
testing.expectEqual('foo', link.href = 'foo');
testing.expectEqual('', link.type);
link.type = 'text/html';
testing.expectEqual('text/html', link.type);
testing.expectEqual('OK', link.text);
link.text = 'foo';
testing.expectEqual('foo', link.text);
</script>

View File

@@ -16,10 +16,11 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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");