Merge pull request #1285 from lightpanda-io/base_url

implement base_url
This commit is contained in:
Karl Seguin
2025-12-24 07:09:39 +08:00
committed by GitHub
19 changed files with 73 additions and 24 deletions

View File

@@ -142,6 +142,11 @@ _queued_navigation: ?QueuedNavigation = null,
// The URL of the current page // The URL of the current page
url: [:0]const u8, url: [:0]const u8,
// The base url specifies the base URL used to resolve the relative urls.
// It is set by a <base> 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 // Arbitrary buffer. Need to temporarily lowercase a value? Use this. No lifetime
// guarantee - it's valid until someone else uses it. // guarantee - it's valid until someone else uses it.
buf: [BUF_SIZE]u8, buf: [BUF_SIZE]u8,
@@ -220,6 +225,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self.version = 0; self.version = 0;
self.url = "about:blank"; self.url = "about:blank";
self.base_url = null;
self.document = (try self._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument(); 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(); try self.registerBackgroundTasks();
} }
pub fn base(self: *const Page) [:0]const u8 {
return self.base_url orelse self.url;
}
fn registerBackgroundTasks(self: *Page) !void { fn registerBackgroundTasks(self: *Page) !void {
if (comptime builtin.is_test) { if (comptime builtin.is_test) {
// HTML test runner manually calls these as necessary // 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( const resolved_url = try URL.resolve(
session.transfer_arena, session.transfer_arena,
self.url, self.base(),
request_url, request_url,
.{ .always_dupe = true }, .{ .always_dupe = true },
); );
@@ -1421,6 +1431,24 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
attribute_iterator, attribute_iterator,
.{ ._proto = undefined }, .{ ._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 => {}, else => {},
}, },
5 => switch (@as(u40, @bitCast(name[0..5].*))) { 5 => switch (@as(u40, @bitCast(name[0..5].*))) {

View File

@@ -190,11 +190,12 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
const page = self.page; const page = self.page;
var source: Script.Source = undefined; var source: Script.Source = undefined;
var remote_url: ?[:0]const u8 = null; var remote_url: ?[:0]const u8 = null;
const base_url = page.base();
if (element.getAttributeSafe("src")) |src| { if (element.getAttributeSafe("src")) |src| {
if (try parseDataURI(page.arena, src)) |data_uri| { if (try parseDataURI(page.arena, src)) |data_uri| {
source = .{ .@"inline" = data_uri }; source = .{ .@"inline" = data_uri };
} else { } else {
remote_url = try URL.resolve(page.arena, page.url, src, .{}); remote_url = try URL.resolve(page.arena, base_url, src, .{});
source = .{ .remote = .{} }; source = .{ .remote = .{} };
} }
} else { } else {
@@ -215,7 +216,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
.script_element = script_element, .script_element = script_element,
.complete = is_inline, .complete = is_inline,
.status = if (is_inline) 200 else 0, .status = if (is_inline) 200 else 0,
.url = remote_url orelse page.url, .url = remote_url orelse base_url,
.mode = blk: { .mode = blk: {
if (source == .@"inline") { if (source == .@"inline") {
break :blk if (kind == .module) .@"defer" else .normal; 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 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_modules_using_import_maps
const resolved_url = try URL.resolve( const resolved_url = try URL.resolve(
self.page.arena, self.page.arena,
self.page.url, self.page.base(),
entry.value_ptr.*, entry.value_ptr.*,
.{}, .{},
); );

View File

@@ -55,7 +55,7 @@ pub fn root(doc: *Node.Document, opts: RootOpts, writer: *std.Io.Writer, page: *
if (opts.with_base) { if (opts.with_base) {
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode(); const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
const base = try doc.createElement("base", null, page); 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); _ = try parent.insertBefore(base.asNode(), parent.firstChild(), page);
} }
} }

View File

@@ -218,7 +218,7 @@ let first_child = content.firstChild.nextSibling; // nextSibling because of line
</script> </script>
<script id=baseURI> <script id=baseURI>
testing.expectEqual('http://localhost:9582/src/tests/dom/node.html', link.baseURI); testing.expectEqual('http://localhost:9589/dom/node.html', link.baseURI);
</script> </script>
<script id=removeChild> <script id=removeChild>

View File

@@ -11,10 +11,10 @@
testing.expectEqual('auto', history.scrollRestoration); testing.expectEqual('auto', history.scrollRestoration);
testing.expectEqual(null, history.state) testing.expectEqual(null, history.state)
history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/src/tests/html/history/history_after_nav.html'); history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9589/html/history/history_after_nav.html');
testing.expectEqual({ testInProgress: true }, history.state); testing.expectEqual({ testInProgress: true }, history.state);
history.pushState({ testInProgress: false }, null, 'http://127.0.0.1:9582/xhr/json'); history.pushState({ testInProgress: false }, null, 'http://127.0.0.1:9589/xhr/json');
history.replaceState({ "new": "field", testComplete: true }, null); history.replaceState({ "new": "field", testComplete: true }, null);
let state = { "new": "field", testComplete: true }; let state = { "new": "field", testComplete: true };

View File

@@ -5,7 +5,7 @@
history.pushState( history.pushState(
{"new": "field", testComplete: true }, {"new": "field", testComplete: true },
null, null,
'http://127.0.0.1:9582/src/tests/html/history/history_after_nav.html' 'http://127.0.0.1:9589/html/history/history_after_nav.html'
); );
let popstateEventFired = false; let popstateEventFired = false;
@@ -16,7 +16,7 @@
popstateEventFired = true; popstateEventFired = true;
popstateEventState = event.state; popstateEventState = event.state;
}; };
testing.eventually(() => { testing.eventually(() => {
testing.expectEqual(true, popstateEventFired); testing.expectEqual(true, popstateEventFired);
testing.expectEqual(true, popstateEventState.testComplete); testing.expectEqual(true, popstateEventState.testComplete);

View File

@@ -54,11 +54,11 @@
testing.expectEqual('', input.src); testing.expectEqual('', input.src);
input.src = 'foo' input.src = 'foo'
testing.expectEqual('http://localhost:9582/src/tests/html/foo', input.src); testing.expectEqual('http://localhost:9589/html/foo', input.src);
input.src = '-3' input.src = '-3'
testing.expectEqual('http://localhost:9582/src/tests/html/-3', input.src); testing.expectEqual('http://localhost:9589/html/-3', input.src);
input.src = '' input.src = ''
testing.expectEqual('http://localhost:9582/src/tests/html/input.html', input.src); testing.expectEqual('http://localhost:9589/html/input.html', input.src);
testing.expectEqual('text', input.type); testing.expectEqual('text', input.type);
input.type = 'checkbox'; input.type = 'checkbox';

View File

@@ -4,7 +4,7 @@
<script id=navigation> <script id=navigation>
testing.expectEqual('object', typeof navigation); testing.expectEqual('object', typeof navigation);
testing.expectEqual('object', typeof navigation.currentEntry); testing.expectEqual('object', typeof navigation.currentEntry);
testing.expectEqual('string', typeof navigation.currentEntry.id); testing.expectEqual('string', typeof navigation.currentEntry.id);
testing.expectEqual('string', typeof navigation.currentEntry.key); testing.expectEqual('string', typeof navigation.currentEntry.key);
testing.expectEqual('string', typeof navigation.currentEntry.url); testing.expectEqual('string', typeof navigation.currentEntry.url);
@@ -12,7 +12,7 @@
const currentIndex = navigation.currentEntry.index; const currentIndex = navigation.currentEntry.index;
navigation.navigate( navigation.navigate(
'http://localhost:9582/src/tests/html/navigation/navigation2.html', 'http://localhost:9589/html/navigation/navigation2.html',
{ state: { currentIndex: currentIndex, navTestInProgress: true } } { state: { currentIndex: currentIndex, navTestInProgress: true } }
); );
</script> </script>

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<base href="https://example.com/">
<a href="foo" id="foo">foo</a>
<script id=baseURI>
testing.expectEqual("http://127.0.0.1:9582/src/browser/tests/node/base_uri.html", document.URL);
testing.expectEqual("https://example.com/", document.baseURI);
const link = $('#foo');
testing.expectEqual("https://example.com/foo", link.href);
</script>

View File

@@ -1178,6 +1178,7 @@ pub const Tag = enum {
body, body,
br, br,
button, button,
base,
canvas, canvas,
circle, circle,
custom, custom,

View File

@@ -874,6 +874,11 @@ pub const JsApi = struct {
fn _toString(self: *const Node) []const u8 { fn _toString(self: *const Node) []const u8 {
return self.className(); 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 { pub const Build = struct {

View File

@@ -44,7 +44,7 @@ pub fn getHref(self: *Anchor, page: *Page) ![]const u8 {
if (href.len == 0) { if (href.len == 0) {
return ""; 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 { 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) { if (href.len == 0) {
return null; 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 { pub const JsApi = struct {

View File

@@ -42,7 +42,7 @@ pub fn getSrc(self: *const Image, page: *Page) ![]const u8 {
} }
// Always resolve the src against the page URL // 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 { pub fn setSrc(self: *Image, value: []const u8, page: *Page) !void {

View File

@@ -42,7 +42,7 @@ pub fn getHref(self: *Link, page: *Page) ![]const u8 {
} }
// Always resolve the href against the page URL // 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 { pub fn setHref(self: *Link, value: []const u8, page: *Page) !void {

View File

@@ -224,7 +224,7 @@ pub fn getSrc(self: *const Media, page: *Page) ![]const u8 {
return ""; return "";
} }
const URL = @import("../../URL.zig"); 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 { pub fn setSrc(self: *Media, value: []const u8, page: *Page) !void {

View File

@@ -59,7 +59,7 @@ pub fn getPoster(self: *const Video, page: *Page) ![]const u8 {
} }
const URL = @import("../../URL.zig"); 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 { pub fn setPoster(self: *Video, value: []const u8, page: *Page) !void {

View File

@@ -69,7 +69,7 @@ pub fn key(self: *const NavigationHistoryEntry) []const u8 {
pub fn sameDocument(self: *const NavigationHistoryEntry, page: *Page) bool { pub fn sameDocument(self: *const NavigationHistoryEntry, page: *Page) bool {
const got_url = self._url orelse return false; 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 { pub fn url(self: *const NavigationHistoryEntry) ?[:0]const u8 {

View File

@@ -69,7 +69,7 @@ const Cache = enum {
pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request { pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request {
const arena = page.arena; const arena = page.arena;
const url = switch (input) { 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), .request => |r| try arena.dupeZ(u8, r._url),
}; };

View File

@@ -130,7 +130,7 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void
self._request_body = null; self._request_body = null;
self._method = try parseMethod(method_); 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); try self.stateChanged(.opened, self._page);
} }