diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 845c7da9..652e60c4 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -194,8 +194,10 @@ pub const HTMLAnchorElement = struct { return try parser.anchorGetHref(self); } - pub fn set_href(self: *parser.Anchor, href: []const u8) !void { - return try parser.anchorSetHref(self, href); + pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void { + const stitch = @import("../../url.zig").stitch; + const full = try stitch(page.call_arena, href, page.url.raw, .{}); + return try parser.anchorSetHref(self, full); } pub fn get_hreflang(self: *parser.Anchor) ![]const u8 { @@ -289,10 +291,9 @@ pub const HTMLAnchorElement = struct { try parser.anchorSetHref(self, href); } - // TODO return a disposable string pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 { var u = try url(self, page); - return try page.arena.dupe(u8, u.get_hostname()); + return u.get_hostname(); } pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void { @@ -326,7 +327,7 @@ pub const HTMLAnchorElement = struct { // TODO return a disposable string pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 { var u = try url(self, page); - return try page.arena.dupe(u8, u.get_username()); + return u.get_username(); } pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { @@ -366,7 +367,7 @@ pub const HTMLAnchorElement = struct { // TODO return a disposable string pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 { var u = try url(self, page); - return try page.arena.dupe(u8, u.get_pathname()); + return u.get_pathname(); } pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void { @@ -1056,62 +1057,62 @@ test "Browser.HTML.Element" { defer runner.deinit(); try runner.testCases(&.{ - .{ "let a = document.getElementById('link')", "undefined" }, - .{ "a.target", "" }, - .{ "a.target = '_blank'", "_blank" }, - .{ "a.target", "_blank" }, - .{ "a.target = ''", "" }, + .{ "let link = document.getElementById('link')", "undefined" }, + .{ "link.target", "" }, + .{ "link.target = '_blank'", "_blank" }, + .{ "link.target", "_blank" }, + .{ "link.target = ''", "" }, - .{ "a.href", "foo" }, - .{ "a.href = 'https://lightpanda.io/'", "https://lightpanda.io/" }, - .{ "a.href", "https://lightpanda.io/" }, + .{ "link.href", "foo" }, + .{ "link.href = 'https://lightpanda.io/'", "https://lightpanda.io/" }, + .{ "link.href", "https://lightpanda.io/" }, - .{ "a.origin", "https://lightpanda.io" }, + .{ "link.origin", "https://lightpanda.io" }, - .{ "a.host = 'lightpanda.io:443'", "lightpanda.io:443" }, - .{ "a.host", "lightpanda.io:443" }, - .{ "a.port", "443" }, - .{ "a.hostname", "lightpanda.io" }, + .{ "link.host = 'lightpanda.io:443'", "lightpanda.io:443" }, + .{ "link.host", "lightpanda.io:443" }, + .{ "link.port", "443" }, + .{ "link.hostname", "lightpanda.io" }, - .{ "a.host = 'lightpanda.io'", "lightpanda.io" }, - .{ "a.host", "lightpanda.io" }, - .{ "a.port", "" }, - .{ "a.hostname", "lightpanda.io" }, + .{ "link.host = 'lightpanda.io'", "lightpanda.io" }, + .{ "link.host", "lightpanda.io" }, + .{ "link.port", "" }, + .{ "link.hostname", "lightpanda.io" }, - .{ "a.host", "lightpanda.io" }, - .{ "a.hostname", "lightpanda.io" }, - .{ "a.hostname = 'foo.bar'", "foo.bar" }, - .{ "a.href", "https://foo.bar/" }, + .{ "link.host", "lightpanda.io" }, + .{ "link.hostname", "lightpanda.io" }, + .{ "link.hostname = 'foo.bar'", "foo.bar" }, + .{ "link.href", "https://foo.bar/" }, - .{ "a.search", "" }, - .{ "a.search = 'q=bar'", "q=bar" }, - .{ "a.search", "?q=bar" }, - .{ "a.href", "https://foo.bar/?q=bar" }, + .{ "link.search", "" }, + .{ "link.search = 'q=bar'", "q=bar" }, + .{ "link.search", "?q=bar" }, + .{ "link.href", "https://foo.bar/?q=bar" }, - .{ "a.hash", "" }, - .{ "a.hash = 'frag'", "frag" }, - .{ "a.hash", "#frag" }, - .{ "a.href", "https://foo.bar/?q=bar#frag" }, + .{ "link.hash", "" }, + .{ "link.hash = 'frag'", "frag" }, + .{ "link.hash", "#frag" }, + .{ "link.href", "https://foo.bar/?q=bar#frag" }, - .{ "a.port", "" }, - .{ "a.port = '443'", "443" }, - .{ "a.host", "foo.bar:443" }, - .{ "a.hostname", "foo.bar" }, - .{ "a.href", "https://foo.bar:443/?q=bar#frag" }, - .{ "a.port = null", "null" }, - .{ "a.href", "https://foo.bar/?q=bar#frag" }, + .{ "link.port", "" }, + .{ "link.port = '443'", "443" }, + .{ "link.host", "foo.bar:443" }, + .{ "link.hostname", "foo.bar" }, + .{ "link.href", "https://foo.bar:443/?q=bar#frag" }, + .{ "link.port = null", "null" }, + .{ "link.href", "https://foo.bar/?q=bar#frag" }, - .{ "a.href = 'foo'", "foo" }, + .{ "link.href = 'foo'", "foo" }, - .{ "a.type", "" }, - .{ "a.type = 'text/html'", "text/html" }, - .{ "a.type", "text/html" }, - .{ "a.type = ''", "" }, + .{ "link.type", "" }, + .{ "link.type = 'text/html'", "text/html" }, + .{ "link.type", "text/html" }, + .{ "link.type = ''", "" }, - .{ "a.text", "OK" }, - .{ "a.text = 'foo'", "foo" }, - .{ "a.text", "foo" }, - .{ "a.text = 'OK'", "OK" }, + .{ "link.text", "OK" }, + .{ "link.text = 'foo'", "foo" }, + .{ "link.text", "foo" }, + .{ "link.text = 'OK'", "OK" }, }, .{}); try runner.testCases(&.{ @@ -1174,4 +1175,10 @@ test "Browser.HTML.Element" { .{ "lyric.src = 15", "15" }, .{ "lyric.src", "15" }, }, .{}); + + try runner.testCases(&.{ + .{ "let a = document.createElement('a');", null }, + .{ "a.href = 'about'", null }, + .{ "a.href", "https://lightpanda.io/opensource-browser/about" }, + }, .{}); } diff --git a/src/url.zig b/src/url.zig index 16f22265..694b9387 100644 --- a/src/url.zig +++ b/src/url.zig @@ -4,6 +4,8 @@ const Uri = std.Uri; const Allocator = std.mem.Allocator; const WebApiURL = @import("browser/url/url.zig").URL; +pub const stitch = URL.stitch; + pub const URL = struct { uri: Uri, raw: []const u8, @@ -91,6 +93,7 @@ pub const URL = struct { if_needed, }; }; + /// Properly stitches two URL fragments together. /// /// For URLs with a path, it will replace the last entry with the src. @@ -101,7 +104,7 @@ pub const URL = struct { base: []const u8, opts: StitchOpts, ) ![]const u8 { - if (base.len == 0) { + if (base.len == 0 or isURL(src)) { if (opts.alloc == .always) { return allocator.dupe(u8, src); } @@ -154,7 +157,41 @@ pub const URL = struct { } }; -test "Url resolve size" { +fn isURL(url: []const u8) bool { + if (std.mem.startsWith(u8, url, "://")) { + return true; + } + + if (url.len < 8) { + return false; + } + + if (!std.ascii.startsWithIgnoreCase(url, "http")) { + return false; + } + + var pos: usize = 4; + if (url[4] == 's' or url[4] == 'S') { + pos = 5; + } + return std.mem.startsWith(u8, url[pos..], "://"); +} + +const testing = @import("testing.zig"); +test "URL: isURL" { + try testing.expectEqual(true, isURL("://lightpanda.io")); + try testing.expectEqual(true, isURL("://lightpanda.io/about")); + try testing.expectEqual(true, isURL("http://lightpanda.io/about")); + try testing.expectEqual(true, isURL("HttP://lightpanda.io/about")); + try testing.expectEqual(true, isURL("httpS://lightpanda.io/about")); + try testing.expectEqual(true, isURL("HTTPs://lightpanda.io/about")); + + try testing.expectEqual(false, isURL("/lightpanda.io")); + try testing.expectEqual(false, isURL("../../about")); + try testing.expectEqual(false, isURL("about")); +} + +test "URL: resolve size" { const base = "https://www.lightpande.io"; const url = try URL.parse(base, null); @@ -170,8 +207,6 @@ test "Url resolve size" { try std.testing.expectEqualStrings(out_url.raw[26..], &url_string); } -const testing = @import("testing.zig"); - test "URL: Stitching Base & Src URLs (Basic)" { const allocator = testing.allocator; @@ -212,6 +247,15 @@ test "URL: Stiching Base & Src URLs (Both Local)" { try testing.expectString("./abcdef/something.js", result); } +test "URL: Stiching src as full path" { + const allocator = testing.allocator; + + const base = "https://www.lightpanda.io/"; + const src = "https://lightpanda.io/something.js"; + const result = try URL.stitch(allocator, src, base, .{}); + try testing.expectString("https://lightpanda.io/something.js", result); +} + test "URL: concatQueryString" { defer testing.reset(); const arena = testing.arena_allocator;