diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index 32b8c232..fd42b083 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -142,6 +142,11 @@ _queued_navigation: ?QueuedNavigation = null,
// The URL of the current page
url: [:0]const u8,
+// The base url specifies the base URL used to resolve the relative urls.
+// It is set by a tag.
+// If null the url must be used.
+base_url: ?[:0]const u8,
+
// Arbitrary buffer. Need to temporarily lowercase a value? Use this. No lifetime
// guarantee - it's valid until someone else uses it.
buf: [BUF_SIZE]u8,
@@ -220,6 +225,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self.version = 0;
self.url = "about:blank";
+ self.base_url = null;
self.document = (try self._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument();
@@ -274,6 +280,10 @@ fn reset(self: *Page, comptime initializing: bool) !void {
try self.registerBackgroundTasks();
}
+pub fn base(self: *const Page) [:0]const u8 {
+ return self.base_url orelse self.url;
+}
+
fn registerBackgroundTasks(self: *Page) !void {
if (comptime builtin.is_test) {
// HTML test runner manually calls these as necessary
@@ -424,7 +434,7 @@ pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOp
const resolved_url = try URL.resolve(
session.transfer_arena,
- self.url,
+ self.base(),
request_url,
.{ .always_dupe = true },
);
@@ -1421,6 +1431,24 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
attribute_iterator,
.{ ._proto = undefined },
),
+ asUint("base") => {
+ const n = try self.createHtmlElementT(
+ Element.Html.Generic,
+ namespace,
+ attribute_iterator,
+ .{ ._proto = undefined, ._tag_name = String.init(undefined, "base", .{}) catch unreachable, ._tag = .base },
+ );
+
+ // If page's base url is not already set, fill it with the base
+ // tag.
+ if (self.base_url == null) {
+ if (n.as(Element).getAttributeSafe("href")) |href| {
+ self.base_url = try URL.resolve(self.arena, self.url, href, .{});
+ }
+ }
+
+ return n;
+ },
else => {},
},
5 => switch (@as(u40, @bitCast(name[0..5].*))) {
diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig
index 1cebeeb5..5f55952f 100644
--- a/src/browser/ScriptManager.zig
+++ b/src/browser/ScriptManager.zig
@@ -190,11 +190,12 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
const page = self.page;
var source: Script.Source = undefined;
var remote_url: ?[:0]const u8 = null;
+ const base_url = page.base();
if (element.getAttributeSafe("src")) |src| {
if (try parseDataURI(page.arena, src)) |data_uri| {
source = .{ .@"inline" = data_uri };
} else {
- remote_url = try URL.resolve(page.arena, page.url, src, .{});
+ remote_url = try URL.resolve(page.arena, base_url, src, .{});
source = .{ .remote = .{} };
}
} else {
@@ -215,7 +216,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
.script_element = script_element,
.complete = is_inline,
.status = if (is_inline) 200 else 0,
- .url = remote_url orelse page.url,
+ .url = remote_url orelse base_url,
.mode = blk: {
if (source == .@"inline") {
break :blk if (kind == .module) .@"defer" else .normal;
@@ -568,7 +569,7 @@ fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_modules_using_import_maps
const resolved_url = try URL.resolve(
self.page.arena,
- self.page.url,
+ self.page.base(),
entry.value_ptr.*,
.{},
);
diff --git a/src/browser/dump.zig b/src/browser/dump.zig
index 77a75c88..bc4d5431 100644
--- a/src/browser/dump.zig
+++ b/src/browser/dump.zig
@@ -55,7 +55,7 @@ pub fn root(doc: *Node.Document, opts: RootOpts, writer: *std.Io.Writer, page: *
if (opts.with_base) {
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
const base = try doc.createElement("base", null, page);
- try base.setAttributeSafe("base", page.url, page);
+ try base.setAttributeSafe("base", page.base(), page);
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), page);
}
}
diff --git a/src/browser/tests/legacy/dom/node.html b/src/browser/tests/legacy/dom/node.html
index ae9b8a3e..c391022a 100644
--- a/src/browser/tests/legacy/dom/node.html
+++ b/src/browser/tests/legacy/dom/node.html
@@ -218,7 +218,7 @@ let first_child = content.firstChild.nextSibling; // nextSibling because of line
diff --git a/src/browser/tests/node/base_uri.html b/src/browser/tests/node/base_uri.html
new file mode 100644
index 00000000..122d7d13
--- /dev/null
+++ b/src/browser/tests/node/base_uri.html
@@ -0,0 +1,14 @@
+
+
+
+
+foo
+
+
+
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index fe5effac..a3a542a8 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -1178,6 +1178,7 @@ pub const Tag = enum {
body,
br,
button,
+ base,
canvas,
circle,
custom,
diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig
index 41357bcc..1149ef44 100644
--- a/src/browser/webapi/Node.zig
+++ b/src/browser/webapi/Node.zig
@@ -874,6 +874,11 @@ pub const JsApi = struct {
fn _toString(self: *const Node) []const u8 {
return self.className();
}
+
+ fn _baseURI(_: *Node, page: *const Page) []const u8 {
+ return page.base();
+ }
+ pub const baseURI = bridge.accessor(_baseURI, null, .{});
};
pub const Build = struct {
diff --git a/src/browser/webapi/element/html/Anchor.zig b/src/browser/webapi/element/html/Anchor.zig
index 75e61c20..44e24823 100644
--- a/src/browser/webapi/element/html/Anchor.zig
+++ b/src/browser/webapi/element/html/Anchor.zig
@@ -44,7 +44,7 @@ pub fn getHref(self: *Anchor, page: *Page) ![]const u8 {
if (href.len == 0) {
return "";
}
- return URL.resolve(page.call_arena, page.url, href, .{});
+ return URL.resolve(page.call_arena, page.base(), href, .{});
}
pub fn setHref(self: *Anchor, value: []const u8, page: *Page) !void {
@@ -195,7 +195,7 @@ fn getResolvedHref(self: *Anchor, page: *Page) !?[:0]const u8 {
if (href.len == 0) {
return null;
}
- return try URL.resolve(page.call_arena, page.url, href, .{});
+ return try URL.resolve(page.call_arena, page.base(), href, .{});
}
pub const JsApi = struct {
diff --git a/src/browser/webapi/element/html/Image.zig b/src/browser/webapi/element/html/Image.zig
index affaaba0..aa6cdd04 100644
--- a/src/browser/webapi/element/html/Image.zig
+++ b/src/browser/webapi/element/html/Image.zig
@@ -42,7 +42,7 @@ pub fn getSrc(self: *const Image, page: *Page) ![]const u8 {
}
// Always resolve the src against the page URL
- return URL.resolve(page.call_arena, page.url, src, .{});
+ return URL.resolve(page.call_arena, page.base(), src, .{});
}
pub fn setSrc(self: *Image, value: []const u8, page: *Page) !void {
diff --git a/src/browser/webapi/element/html/Link.zig b/src/browser/webapi/element/html/Link.zig
index e381e227..f8607ca8 100644
--- a/src/browser/webapi/element/html/Link.zig
+++ b/src/browser/webapi/element/html/Link.zig
@@ -42,7 +42,7 @@ pub fn getHref(self: *Link, page: *Page) ![]const u8 {
}
// Always resolve the href against the page URL
- return URL.resolve(page.call_arena, page.url, href, .{});
+ return URL.resolve(page.call_arena, page.base(), href, .{});
}
pub fn setHref(self: *Link, value: []const u8, page: *Page) !void {
diff --git a/src/browser/webapi/element/html/Media.zig b/src/browser/webapi/element/html/Media.zig
index dc29e160..eca130d0 100644
--- a/src/browser/webapi/element/html/Media.zig
+++ b/src/browser/webapi/element/html/Media.zig
@@ -224,7 +224,7 @@ pub fn getSrc(self: *const Media, page: *Page) ![]const u8 {
return "";
}
const URL = @import("../../URL.zig");
- return URL.resolve(page.call_arena, page.url, src, .{});
+ return URL.resolve(page.call_arena, page.base(), src, .{});
}
pub fn setSrc(self: *Media, value: []const u8, page: *Page) !void {
diff --git a/src/browser/webapi/element/html/Video.zig b/src/browser/webapi/element/html/Video.zig
index cfe19da0..dda22eb2 100644
--- a/src/browser/webapi/element/html/Video.zig
+++ b/src/browser/webapi/element/html/Video.zig
@@ -59,7 +59,7 @@ pub fn getPoster(self: *const Video, page: *Page) ![]const u8 {
}
const URL = @import("../../URL.zig");
- return URL.resolve(page.call_arena, page.url, poster, .{});
+ return URL.resolve(page.call_arena, page.base(), poster, .{});
}
pub fn setPoster(self: *Video, value: []const u8, page: *Page) !void {
diff --git a/src/browser/webapi/navigation/NavigationHistoryEntry.zig b/src/browser/webapi/navigation/NavigationHistoryEntry.zig
index 2411a741..5e1a98a8 100644
--- a/src/browser/webapi/navigation/NavigationHistoryEntry.zig
+++ b/src/browser/webapi/navigation/NavigationHistoryEntry.zig
@@ -69,7 +69,7 @@ pub fn key(self: *const NavigationHistoryEntry) []const u8 {
pub fn sameDocument(self: *const NavigationHistoryEntry, page: *Page) bool {
const got_url = self._url orelse return false;
- return URL.eqlDocument(got_url, page.url);
+ return URL.eqlDocument(got_url, page.base());
}
pub fn url(self: *const NavigationHistoryEntry) ?[:0]const u8 {
diff --git a/src/browser/webapi/net/Request.zig b/src/browser/webapi/net/Request.zig
index 13c7a977..46cad2a1 100644
--- a/src/browser/webapi/net/Request.zig
+++ b/src/browser/webapi/net/Request.zig
@@ -69,7 +69,7 @@ const Cache = enum {
pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request {
const arena = page.arena;
const url = switch (input) {
- .url => |u| try URL.resolve(arena, page.url, u, .{ .always_dupe = true }),
+ .url => |u| try URL.resolve(arena, page.base(), u, .{ .always_dupe = true }),
.request => |r| try arena.dupeZ(u8, r._url),
};
diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig
index c0837680..a29476a6 100644
--- a/src/browser/webapi/net/XMLHttpRequest.zig
+++ b/src/browser/webapi/net/XMLHttpRequest.zig
@@ -130,7 +130,7 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void
self._request_body = null;
self._method = try parseMethod(method_);
- self._url = try URL.resolve(self._arena, self._page.url, url, .{ .always_dupe = true });
+ self._url = try URL.resolve(self._arena, self._page.base(), url, .{ .always_dupe = true });
try self.stateChanged(.opened, self._page);
}