mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-14 15:28:57 +00:00
Tweak URL, refactor Anchor and URL to share more common code
This commit is contained in:
@@ -128,7 +128,7 @@ fn isNullTerminated(comptime value: type) bool {
|
||||
}
|
||||
|
||||
pub fn isCompleteHTTPUrl(url: []const u8) bool {
|
||||
if (url.len < 6) {
|
||||
if (url.len < 3) { // Minimum is "x://"
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -137,9 +137,32 @@ pub fn isCompleteHTTPUrl(url: []const u8) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
return std.ascii.startsWithIgnoreCase(url, "https://") or
|
||||
std.ascii.startsWithIgnoreCase(url, "http://") or
|
||||
std.ascii.startsWithIgnoreCase(url, "ftp://");
|
||||
// Check if there's a scheme (protocol) ending with ://
|
||||
const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false;
|
||||
|
||||
// Check if it's followed by //
|
||||
if (colon_pos + 2 >= url.len or url[colon_pos + 1] != '/' or url[colon_pos + 2] != '/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate that everything before the colon is a valid scheme
|
||||
// A scheme must start with a letter and contain only letters, digits, +, -, .
|
||||
if (colon_pos == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const scheme = url[0..colon_pos];
|
||||
if (!std.ascii.isAlphabetic(scheme[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (scheme[1..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '+' and c != '-' and c != '.') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn getUsername(raw: [:0]const u8) []const u8 {
|
||||
@@ -278,6 +301,139 @@ pub fn eqlDocument(first: [:0]const u8, second: [:0]const u8) bool {
|
||||
return std.mem.eql(u8, first[0..first_hash_index], second[0..second_hash_index]);
|
||||
}
|
||||
|
||||
// Helper function to build a URL from components
|
||||
pub fn buildUrl(
|
||||
allocator: 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 fn setProtocol(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
||||
const host = getHost(current);
|
||||
const pathname = getPathname(current);
|
||||
const search = getSearch(current);
|
||||
const hash = getHash(current);
|
||||
|
||||
// Add : suffix if not present
|
||||
const protocol = if (value.len > 0 and value[value.len - 1] != ':')
|
||||
try std.fmt.allocPrint(allocator, "{s}:", .{value})
|
||||
else
|
||||
value;
|
||||
|
||||
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
||||
}
|
||||
|
||||
pub fn setHost(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
||||
const protocol = getProtocol(current);
|
||||
const pathname = getPathname(current);
|
||||
const search = getSearch(current);
|
||||
const hash = getHash(current);
|
||||
|
||||
// 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;
|
||||
|
||||
return buildUrl(allocator, protocol, clean_host, pathname, search, hash);
|
||||
}
|
||||
|
||||
pub fn setHostname(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
||||
const current_port = getPort(current);
|
||||
const new_host = if (current_port.len > 0)
|
||||
try std.fmt.allocPrint(allocator, "{s}:{s}", .{ value, current_port })
|
||||
else
|
||||
value;
|
||||
|
||||
return setHost(current, new_host, allocator);
|
||||
}
|
||||
|
||||
pub fn setPort(current: [:0]const u8, value: ?[]const u8, allocator: Allocator) ![:0]const u8 {
|
||||
const hostname = getHostname(current);
|
||||
const protocol = getProtocol(current);
|
||||
|
||||
// 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(allocator, "{s}:{s}", .{ hostname, port_str });
|
||||
} else hostname;
|
||||
|
||||
return setHost(current, new_host, allocator);
|
||||
}
|
||||
|
||||
pub fn setPathname(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
||||
const protocol = getProtocol(current);
|
||||
const host = getHost(current);
|
||||
const search = getSearch(current);
|
||||
const hash = getHash(current);
|
||||
|
||||
// Add / prefix if not present and value is not empty
|
||||
const pathname = if (value.len > 0 and value[0] != '/')
|
||||
try std.fmt.allocPrint(allocator, "/{s}", .{value})
|
||||
else
|
||||
value;
|
||||
|
||||
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
||||
}
|
||||
|
||||
pub fn setSearch(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
||||
const protocol = getProtocol(current);
|
||||
const host = getHost(current);
|
||||
const pathname = getPathname(current);
|
||||
const hash = getHash(current);
|
||||
|
||||
// Add ? prefix if not present and value is not empty
|
||||
const search = if (value.len > 0 and value[0] != '?')
|
||||
try std.fmt.allocPrint(allocator, "?{s}", .{value})
|
||||
else
|
||||
value;
|
||||
|
||||
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
||||
}
|
||||
|
||||
pub fn setHash(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
||||
const protocol = getProtocol(current);
|
||||
const host = getHost(current);
|
||||
const pathname = getPathname(current);
|
||||
const search = getSearch(current);
|
||||
|
||||
// Add # prefix if not present and value is not empty
|
||||
const hash = if (value.len > 0 and value[0] != '#')
|
||||
try std.fmt.allocPrint(allocator, "#{s}", .{value})
|
||||
else
|
||||
value;
|
||||
|
||||
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
||||
}
|
||||
|
||||
const KnownProtocol = enum {
|
||||
@"http:",
|
||||
@"https:",
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
</script>
|
||||
|
||||
<script id="constructor">
|
||||
testing.expectError("TypeError: invalid argument", () => {
|
||||
testing.expectError("Error: TypeError", () => {
|
||||
new URL(document.createElement('a'));
|
||||
});
|
||||
|
||||
let a = document.createElement('a');
|
||||
a.href = 'https://www.lightpanda.io/over?9000=!!';
|
||||
const url3 = new URL(a);
|
||||
testing.expectEqual("https://www.lightpanda.io/over?9000=%21%21", url3.href);
|
||||
testing.expectEqual("https://www.lightpanda.io/over?9000=!!", url3.href);
|
||||
</script>
|
||||
|
||||
<script id=searchParams>
|
||||
@@ -53,10 +53,10 @@
|
||||
testing.expectEqual(3, url.searchParams.size);
|
||||
|
||||
// search is dynamic
|
||||
testing.expectEqual("?a=~&b=~&c=foo", url.search);
|
||||
testing.expectEqual("?a=%7E&b=%7E&c=foo", url.search);
|
||||
|
||||
// href is dynamic
|
||||
testing.expectEqual("https://foo.bar/path?a=~&b=~&c=foo#fragment", url.href);
|
||||
testing.expectEqual("https://foo.bar/path?a=%7E&b=%7E&c=foo#fragment", url.href);
|
||||
|
||||
url.searchParams.delete('c', 'foo');
|
||||
testing.expectEqual(null, url.searchParams.get('c'));
|
||||
@@ -95,7 +95,7 @@
|
||||
</script>
|
||||
|
||||
<script id=invalidUrl>
|
||||
testing.expectError("Error: Invalid", () => {
|
||||
testing.expectError("Error: TypeError", () => {
|
||||
_ = new URL("://foo.bar/path?query#fragment");
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -313,6 +313,23 @@
|
||||
url.searchParams.delete('b');
|
||||
testing.expectEqual('https://example.com/path', url.href);
|
||||
}
|
||||
|
||||
{
|
||||
let url = new URL("https://foo.bar");
|
||||
const searchParams = url.searchParams;
|
||||
|
||||
// SearchParams should be empty.
|
||||
testing.expectEqual(0, searchParams.size);
|
||||
|
||||
url.href = "https://lightpanda.io?over=9000&light=panda";
|
||||
// It won't hurt to check href and host too.
|
||||
testing.expectEqual("https://lightpanda.io/?over=9000&light=panda", url.href);
|
||||
testing.expectEqual("lightpanda.io", url.host);
|
||||
// SearchParams should be updated too when URL is set.
|
||||
testing.expectEqual(2, searchParams.size);
|
||||
testing.expectEqual("9000", searchParams.get("over"));
|
||||
testing.expectEqual("panda", searchParams.get("light"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=stringifier>
|
||||
@@ -362,3 +379,280 @@
|
||||
testing.expectEqual('https://example.com/base/relative', url.href);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=setters>
|
||||
{
|
||||
const url = new URL('https://example.com/path');
|
||||
url.href = 'https://newdomain.com/newpath';
|
||||
testing.expectEqual('https://newdomain.com/newpath', url.href);
|
||||
testing.expectEqual('newdomain.com', url.hostname);
|
||||
testing.expectEqual('/newpath', url.pathname);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path?a=b#hash');
|
||||
url.href = 'http://other.com/';
|
||||
testing.expectEqual('http://other.com/', url.href);
|
||||
testing.expectEqual('', url.search);
|
||||
testing.expectEqual('', url.hash);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path');
|
||||
url.protocol = 'http';
|
||||
testing.expectEqual('http://example.com/path', url.href);
|
||||
testing.expectEqual('http:', url.protocol);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('http://example.com/path');
|
||||
url.protocol = 'https:';
|
||||
testing.expectEqual('https://example.com/path', url.href);
|
||||
testing.expectEqual('https:', url.protocol);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path');
|
||||
url.hostname = 'newhost.com';
|
||||
testing.expectEqual('https://newhost.com/path', url.href);
|
||||
testing.expectEqual('newhost.com', url.hostname);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com:8080/path');
|
||||
url.hostname = 'newhost.com';
|
||||
testing.expectEqual('https://newhost.com:8080/path', url.href);
|
||||
testing.expectEqual('newhost.com', url.hostname);
|
||||
testing.expectEqual('8080', url.port);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path');
|
||||
url.host = 'newhost.com:9000';
|
||||
testing.expectEqual('https://newhost.com:9000/path', url.href);
|
||||
testing.expectEqual('newhost.com', url.hostname);
|
||||
testing.expectEqual('9000', url.port);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com:8080/path');
|
||||
url.host = 'newhost.com';
|
||||
testing.expectEqual('https://newhost.com/path', url.href);
|
||||
testing.expectEqual('newhost.com', url.hostname);
|
||||
testing.expectEqual('', url.port);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path');
|
||||
url.port = '8080';
|
||||
testing.expectEqual('https://example.com:8080/path', url.href);
|
||||
testing.expectEqual('8080', url.port);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com:8080/path');
|
||||
url.port = '';
|
||||
testing.expectEqual('https://example.com/path', url.href);
|
||||
testing.expectEqual('', url.port);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path');
|
||||
url.port = '443';
|
||||
testing.expectEqual('https://example.com/path', url.href);
|
||||
testing.expectEqual('', url.port);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('http://example.com/path');
|
||||
url.port = '80';
|
||||
testing.expectEqual('http://example.com/path', url.href);
|
||||
testing.expectEqual('', url.port);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path');
|
||||
url.pathname = '/newpath';
|
||||
testing.expectEqual('https://example.com/newpath', url.href);
|
||||
testing.expectEqual('/newpath', url.pathname);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path');
|
||||
url.pathname = 'newpath';
|
||||
testing.expectEqual('https://example.com/newpath', url.href);
|
||||
testing.expectEqual('/newpath', url.pathname);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path?a=b#hash');
|
||||
url.pathname = '/new/path';
|
||||
testing.expectEqual('https://example.com/new/path?a=b#hash', url.href);
|
||||
testing.expectEqual('/new/path', url.pathname);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path');
|
||||
url.search = '?a=b';
|
||||
testing.expectEqual('https://example.com/path?a=b', url.href);
|
||||
testing.expectEqual('?a=b', url.search);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path');
|
||||
url.search = 'a=b';
|
||||
testing.expectEqual('https://example.com/path?a=b', url.href);
|
||||
testing.expectEqual('?a=b', url.search);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path?old=value');
|
||||
url.search = 'new=value';
|
||||
testing.expectEqual('https://example.com/path?new=value', url.href);
|
||||
testing.expectEqual('?new=value', url.search);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path?a=b#hash');
|
||||
url.search = 'c=d';
|
||||
testing.expectEqual('https://example.com/path?c=d#hash', url.href);
|
||||
testing.expectEqual('?c=d', url.search);
|
||||
testing.expectEqual('#hash', url.hash);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path');
|
||||
url.hash = '#section';
|
||||
testing.expectEqual('https://example.com/path#section', url.href);
|
||||
testing.expectEqual('#section', url.hash);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path');
|
||||
url.hash = 'section';
|
||||
testing.expectEqual('https://example.com/path#section', url.href);
|
||||
testing.expectEqual('#section', url.hash);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path#old');
|
||||
url.hash = 'new';
|
||||
testing.expectEqual('https://example.com/path#new', url.href);
|
||||
testing.expectEqual('#new', url.hash);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path?a=b#hash');
|
||||
url.hash = 'newhash';
|
||||
testing.expectEqual('https://example.com/path?a=b#newhash', url.href);
|
||||
testing.expectEqual('?a=b', url.search);
|
||||
testing.expectEqual('#newhash', url.hash);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path#hash');
|
||||
url.hash = '';
|
||||
testing.expectEqual('https://example.com/path', url.href);
|
||||
testing.expectEqual('', url.hash);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path?a=b');
|
||||
url.search = '';
|
||||
testing.expectEqual('https://example.com/path', url.href);
|
||||
testing.expectEqual('', url.search);
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path?a=b');
|
||||
const sp = url.searchParams;
|
||||
testing.expectEqual('b', sp.get('a'));
|
||||
url.search = 'c=d';
|
||||
|
||||
testing.expectEqual('d', url.searchParams.get('c'));
|
||||
testing.expectEqual(null, url.searchParams.get('a'));
|
||||
}
|
||||
|
||||
{
|
||||
const url = new URL('https://example.com/path?a=b');
|
||||
const sp = url.searchParams;
|
||||
testing.expectEqual('b', sp.get('a'));
|
||||
url.href = 'https://example.com/other?x=y';
|
||||
|
||||
testing.expectEqual('y', url.searchParams.get('x'));
|
||||
testing.expectEqual(null, url.searchParams.get('a'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=canParse>
|
||||
{
|
||||
testing.expectEqual('function', typeof URL.canParse);
|
||||
}
|
||||
|
||||
{
|
||||
testing.expectEqual(true, URL.canParse('https://example.com'));
|
||||
testing.expectEqual(true, URL.canParse('http://example.com/path'));
|
||||
testing.expectEqual(true, URL.canParse('https://example.com:8080/path?a=b#hash'));
|
||||
testing.expectEqual(true, URL.canParse('ftp://example.com'));
|
||||
}
|
||||
|
||||
{
|
||||
testing.expectEqual(false, URL.canParse(''));
|
||||
testing.expectEqual(false, URL.canParse('foo.js'));
|
||||
testing.expectEqual(false, URL.canParse('/path/to/file'));
|
||||
testing.expectEqual(false, URL.canParse('?query=value'));
|
||||
testing.expectEqual(false, URL.canParse('#fragment'));
|
||||
}
|
||||
|
||||
{
|
||||
testing.expectEqual(true, URL.canParse('subpath', 'http://example.com/path/'));
|
||||
testing.expectEqual(true, URL.canParse('/newpath', 'http://example.com/path'));
|
||||
testing.expectEqual(true, URL.canParse('?a=b', 'http://example.com/path'));
|
||||
testing.expectEqual(true, URL.canParse('#hash', 'http://example.com/path'));
|
||||
testing.expectEqual(true, URL.canParse('../foo', 'http://example.com/a/b/c'));
|
||||
}
|
||||
|
||||
{
|
||||
testing.expectEqual(false, URL.canParse('foo.js', undefined));
|
||||
testing.expectEqual(false, URL.canParse('subpath', undefined));
|
||||
testing.expectEqual(false, URL.canParse('../foo', undefined));
|
||||
}
|
||||
|
||||
{
|
||||
testing.expectEqual(false, URL.canParse('foo.js', 'not a url'));
|
||||
testing.expectEqual(false, URL.canParse('foo.js', 'bar/baz'));
|
||||
testing.expectEqual(false, URL.canParse('subpath', 'relative/path'));
|
||||
}
|
||||
|
||||
{
|
||||
testing.expectEqual(false, URL.canParse('http://example.com/absolute', 'invalid base'));
|
||||
testing.expectEqual(false, URL.canParse('https://example.com/path', 'not-a-url'));
|
||||
}
|
||||
|
||||
{
|
||||
testing.expectEqual(false, URL.canParse('http://example.com', ''));
|
||||
testing.expectEqual(false, URL.canParse('http://example.com', 'relative'));
|
||||
}
|
||||
|
||||
{
|
||||
testing.expectEqual(true, URL.canParse('http://example.com/absolute', 'http://base.com'));
|
||||
testing.expectEqual(true, URL.canParse('https://example.com/path', 'https://other.com'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=query_encode>
|
||||
{
|
||||
let url = new URL('https://foo.bar/path?a=~&b=%7E#fragment');
|
||||
testing.expectEqual("~", url.searchParams.get('a'));
|
||||
testing.expectEqual("~", url.searchParams.get('b'));
|
||||
|
||||
url.searchParams.append('c', 'foo');
|
||||
testing.expectEqual("foo", url.searchParams.get('c'));
|
||||
testing.expectEqual(1, url.searchParams.getAll('c').length);
|
||||
testing.expectEqual("foo", url.searchParams.getAll('c')[0]);
|
||||
testing.expectEqual(3, url.searchParams.size);
|
||||
|
||||
// search is dynamic
|
||||
testing.expectEqual("?a=%7E&b=%7E&c=foo", url.search);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -57,8 +57,8 @@ pub fn getOrigin(self: *const Location, page: *const Page) ![]const u8 {
|
||||
return self._url.getOrigin(page);
|
||||
}
|
||||
|
||||
pub fn getSearch(self: *const Location) []const u8 {
|
||||
return self._url.getSearch();
|
||||
pub fn getSearch(self: *const Location, page: *const Page) ![]const u8 {
|
||||
return self._url.getSearch(page);
|
||||
}
|
||||
|
||||
pub fn getHash(self: *const Location) []const u8 {
|
||||
|
||||
@@ -35,6 +35,23 @@ _search_params: ?*URLSearchParams = null,
|
||||
pub const resolve = @import("../URL.zig").resolve;
|
||||
pub const eqlDocument = @import("../URL.zig").eqlDocument;
|
||||
|
||||
pub fn canParse(url: []const u8, base_: ?[]const u8, page: *Page) bool {
|
||||
_ = page;
|
||||
const url_is_absolute = U.isCompleteHTTPUrl(url);
|
||||
|
||||
if (base_) |b| {
|
||||
// Base must be valid even if URL is absolute
|
||||
if (!U.isCompleteHTTPUrl(b)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else if (!url_is_absolute) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL {
|
||||
const url_is_absolute = @import("../URL.zig").isCompleteHTTPUrl(url);
|
||||
|
||||
@@ -96,7 +113,17 @@ pub fn getOrigin(self: *const URL, page: *const Page) ![]const u8 {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getSearch(self: *const URL) []const u8 {
|
||||
pub fn getSearch(self: *const URL, page: *const Page) ![]const u8 {
|
||||
// If searchParams has been accessed, generate search from it
|
||||
if (self._search_params) |sp| {
|
||||
if (sp.getSize() == 0) {
|
||||
return "";
|
||||
}
|
||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try buf.writer.writeByte('?');
|
||||
try sp.toString(&buf.writer);
|
||||
return buf.written();
|
||||
}
|
||||
return U.getSearch(self._raw);
|
||||
}
|
||||
|
||||
@@ -110,7 +137,7 @@ pub fn getSearchParams(self: *URL, page: *Page) !*URLSearchParams {
|
||||
}
|
||||
|
||||
// Get current search string (without the '?')
|
||||
const search = self.getSearch();
|
||||
const search = try self.getSearch(page);
|
||||
const search_value = if (search.len > 0) search[1..] else "";
|
||||
|
||||
const params = try URLSearchParams.init(.{ .query_string = search_value }, page);
|
||||
@@ -118,6 +145,61 @@ pub fn getSearchParams(self: *URL, page: *Page) !*URLSearchParams {
|
||||
return params;
|
||||
}
|
||||
|
||||
pub fn setHref(self: *URL, value: []const u8, page: *Page) !void {
|
||||
const base = if (U.isCompleteHTTPUrl(value)) page.url else self._raw;
|
||||
const raw = try U.resolve(self._arena orelse page.arena, base, value, .{ .always_dupe = true });
|
||||
self._raw = raw;
|
||||
|
||||
// Update existing searchParams if it exists
|
||||
if (self._search_params) |sp| {
|
||||
const search = U.getSearch(raw);
|
||||
const search_value = if (search.len > 0) search[1..] else "";
|
||||
try sp.updateFromString(search_value, page);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setProtocol(self: *URL, value: []const u8) !void {
|
||||
const allocator = self._arena orelse return error.NoAllocator;
|
||||
self._raw = try U.setProtocol(self._raw, value, allocator);
|
||||
}
|
||||
|
||||
pub fn setHost(self: *URL, value: []const u8) !void {
|
||||
const allocator = self._arena orelse return error.NoAllocator;
|
||||
self._raw = try U.setHost(self._raw, value, allocator);
|
||||
}
|
||||
|
||||
pub fn setHostname(self: *URL, value: []const u8) !void {
|
||||
const allocator = self._arena orelse return error.NoAllocator;
|
||||
self._raw = try U.setHostname(self._raw, value, allocator);
|
||||
}
|
||||
|
||||
pub fn setPort(self: *URL, value: ?[]const u8) !void {
|
||||
const allocator = self._arena orelse return error.NoAllocator;
|
||||
self._raw = try U.setPort(self._raw, value, allocator);
|
||||
}
|
||||
|
||||
pub fn setPathname(self: *URL, value: []const u8) !void {
|
||||
const allocator = self._arena orelse return error.NoAllocator;
|
||||
self._raw = try U.setPathname(self._raw, value, allocator);
|
||||
}
|
||||
|
||||
pub fn setSearch(self: *URL, value: []const u8, page: *Page) !void {
|
||||
const allocator = self._arena orelse return error.NoAllocator;
|
||||
self._raw = try U.setSearch(self._raw, value, allocator);
|
||||
|
||||
// Update existing searchParams if it exists
|
||||
if (self._search_params) |sp| {
|
||||
const search = U.getSearch(self._raw);
|
||||
const search_value = if (search.len > 0) search[1..] else "";
|
||||
try sp.updateFromString(search_value, page);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setHash(self: *URL, value: []const u8) !void {
|
||||
const allocator = self._arena orelse return error.NoAllocator;
|
||||
self._raw = try U.setHash(self._raw, value, allocator);
|
||||
}
|
||||
|
||||
pub fn toString(self: *const URL, page: *const Page) ![:0]const u8 {
|
||||
const sp = self._search_params orelse {
|
||||
return self._raw;
|
||||
@@ -137,6 +219,13 @@ pub fn toString(self: *const URL, page: *const Page) ![:0]const u8 {
|
||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try buf.writer.writeAll(base);
|
||||
|
||||
// Add / if missing (e.g., "https://example.com" -> "https://example.com/")
|
||||
// Only add if pathname is just "/" and not already in the base
|
||||
const pathname = U.getPathname(raw);
|
||||
if (std.mem.eql(u8, pathname, "/") and !std.mem.endsWith(u8, base, "/")) {
|
||||
try buf.writer.writeByte('/');
|
||||
}
|
||||
|
||||
// Only add ? if there are params
|
||||
if (sp.getSize() > 0) {
|
||||
try buf.writer.writeByte('?');
|
||||
@@ -159,19 +248,20 @@ pub const JsApi = struct {
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(URL.init, .{});
|
||||
pub const canParse = bridge.function(URL.canParse, .{ .static = true });
|
||||
pub const toString = bridge.function(URL.toString, .{});
|
||||
pub const toJSON = bridge.function(URL.toString, .{});
|
||||
pub const href = bridge.accessor(URL.toString, null, .{});
|
||||
pub const search = bridge.accessor(URL.getSearch, null, .{});
|
||||
pub const hash = bridge.accessor(URL.getHash, null, .{});
|
||||
pub const pathname = bridge.accessor(URL.getPathname, null, .{});
|
||||
pub const href = bridge.accessor(URL.toString, URL.setHref, .{});
|
||||
pub const search = bridge.accessor(URL.getSearch, URL.setSearch, .{});
|
||||
pub const hash = bridge.accessor(URL.getHash, URL.setHash, .{});
|
||||
pub const pathname = bridge.accessor(URL.getPathname, URL.setPathname, .{});
|
||||
pub const username = bridge.accessor(URL.getUsername, null, .{});
|
||||
pub const password = bridge.accessor(URL.getPassword, null, .{});
|
||||
pub const hostname = bridge.accessor(URL.getHostname, null, .{});
|
||||
pub const host = bridge.accessor(URL.getHost, null, .{});
|
||||
pub const port = bridge.accessor(URL.getPort, null, .{});
|
||||
pub const hostname = bridge.accessor(URL.getHostname, URL.setHostname, .{});
|
||||
pub const host = bridge.accessor(URL.getHost, URL.setHost, .{});
|
||||
pub const port = bridge.accessor(URL.getPort, URL.setPort, .{});
|
||||
pub const origin = bridge.accessor(URL.getOrigin, null, .{});
|
||||
pub const protocol = bridge.accessor(URL.getProtocol, null, .{});
|
||||
pub const protocol = bridge.accessor(URL.getProtocol, URL.setProtocol, .{});
|
||||
pub const searchParams = bridge.accessor(URL.getSearchParams, null, .{});
|
||||
};
|
||||
|
||||
|
||||
@@ -84,26 +84,7 @@ pub fn getHost(self: *Anchor, page: *Page) ![]const u8 {
|
||||
|
||||
pub fn setHost(self: *Anchor, value: []const u8, page: *Page) !void {
|
||||
const href = try getResolvedHref(self, page) orelse return;
|
||||
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);
|
||||
const new_href = try URL.setHost(href, value, page.call_arena);
|
||||
try setHref(self, new_href, page);
|
||||
}
|
||||
|
||||
@@ -114,13 +95,8 @@ pub fn getHostname(self: *Anchor, page: *Page) ![]const u8 {
|
||||
|
||||
pub fn setHostname(self: *Anchor, value: []const u8, page: *Page) !void {
|
||||
const href = try getResolvedHref(self, page) orelse return;
|
||||
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);
|
||||
const new_href = try URL.setHostname(href, value, page.call_arena);
|
||||
try setHref(self, new_href, page);
|
||||
}
|
||||
|
||||
pub fn getPort(self: *Anchor, page: *Page) ![]const u8 {
|
||||
@@ -142,25 +118,8 @@ pub fn getPort(self: *Anchor, page: *Page) ![]const u8 {
|
||||
|
||||
pub fn setPort(self: *Anchor, value: ?[]const u8, page: *Page) !void {
|
||||
const href = try getResolvedHref(self, page) orelse return;
|
||||
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);
|
||||
const new_href = try URL.setPort(href, value, page.call_arena);
|
||||
try setHref(self, new_href, page);
|
||||
}
|
||||
|
||||
pub fn getSearch(self: *Anchor, page: *Page) ![]const u8 {
|
||||
@@ -170,18 +129,7 @@ pub fn getSearch(self: *Anchor, page: *Page) ![]const u8 {
|
||||
|
||||
pub fn setSearch(self: *Anchor, value: []const u8, page: *Page) !void {
|
||||
const href = try getResolvedHref(self, page) orelse return;
|
||||
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);
|
||||
const new_href = try URL.setSearch(href, value, page.call_arena);
|
||||
try setHref(self, new_href, page);
|
||||
}
|
||||
|
||||
@@ -192,18 +140,7 @@ pub fn getHash(self: *Anchor, page: *Page) ![]const u8 {
|
||||
|
||||
pub fn setHash(self: *Anchor, value: []const u8, page: *Page) !void {
|
||||
const href = try getResolvedHref(self, page) orelse return;
|
||||
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);
|
||||
const new_href = try URL.setHash(href, value, page.call_arena);
|
||||
try setHref(self, new_href, page);
|
||||
}
|
||||
|
||||
@@ -214,18 +151,7 @@ pub fn getPathname(self: *Anchor, page: *Page) ![]const u8 {
|
||||
|
||||
pub fn setPathname(self: *Anchor, value: []const u8, page: *Page) !void {
|
||||
const href = try getResolvedHref(self, page) orelse return;
|
||||
const protocol = URL.getProtocol(href);
|
||||
const host = URL.getHost(href);
|
||||
const search = URL.getSearch(href);
|
||||
const hash = URL.getHash(href);
|
||||
|
||||
// Add / prefix if not present and value is not empty
|
||||
const pathname = 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);
|
||||
const new_href = try URL.setPathname(href, value, page.call_arena);
|
||||
try setHref(self, new_href, page);
|
||||
}
|
||||
|
||||
@@ -236,18 +162,7 @@ pub fn getProtocol(self: *Anchor, page: *Page) ![]const u8 {
|
||||
|
||||
pub fn setProtocol(self: *Anchor, value: []const u8, page: *Page) !void {
|
||||
const href = try getResolvedHref(self, page) orelse return;
|
||||
const host = URL.getHost(href);
|
||||
const pathname = URL.getPathname(href);
|
||||
const search = URL.getSearch(href);
|
||||
const hash = URL.getHash(href);
|
||||
|
||||
// Add : suffix if not present
|
||||
const protocol = if (value.len > 0 and value[value.len - 1] != ':')
|
||||
try std.fmt.allocPrint(page.call_arena, "{s}:", .{value})
|
||||
else
|
||||
value;
|
||||
|
||||
const new_href = try buildUrl(page.call_arena, protocol, host, pathname, search, hash);
|
||||
const new_href = try URL.setProtocol(href, value, page.call_arena);
|
||||
try setHref(self, new_href, page);
|
||||
}
|
||||
|
||||
@@ -283,24 +198,6 @@ fn getResolvedHref(self: *Anchor, page: *Page) !?[:0]const u8 {
|
||||
return try URL.resolve(page.call_arena, page.url, href, .{});
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -61,6 +61,10 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn updateFromString(self: *URLSearchParams, query_string: []const u8, page: *Page) !void {
|
||||
self._params = try paramsFromString(self._arena, query_string, &page.buf);
|
||||
}
|
||||
|
||||
pub fn getSize(self: *const URLSearchParams) usize {
|
||||
return self._params.len();
|
||||
}
|
||||
@@ -277,7 +281,7 @@ fn escape(input: []const u8, writer: *std.Io.Writer) !void {
|
||||
|
||||
fn isUnreserved(c: u8) bool {
|
||||
return switch (c) {
|
||||
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
|
||||
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_' => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user