From 9eb229a963f67f4d6903bd456f37689f9ba07e2d Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 28 May 2026 16:26:09 +0800 Subject: [PATCH] Improve ImporMap This is driven by import-maps WPT tests. It generally does 3 high level additions, plus various small compliance tweaks. 1 - Entries with a trailing match are used for prefix matching 2 - Supports scopes (which are entries group into a specific route-like prefix) 3 - Because of the above, resolves in correct order with fallback Resolution is based on longest-match wins, so it doesn't require a fancy data structures. We use ordered (by length) slices, and just iterate until we find a match. Neither our parsing nor our matching is super efficient. While a page might have hundreds of scripts, it likely has only 0-1 import maps and relatively few values. ImportMap.resolve always returns the final URL. So even if a match isn't found based on the parsed JSON, it'll return the URL.resolve(base, url). Just to make WPT tests pass, we do have to track invalid entries in the ImportMap, e.g. "key": "not-a-url". In the previous version, we'd fallback to URL.resolve(base, url). Now we return null and leave it to the caller to decide. --- src/browser/ImportMap.zig | 535 +++++++++++++++++++++++ src/browser/ScriptManager.zig | 31 +- src/browser/ScriptManagerBase.zig | 32 +- src/browser/URL.zig | 62 ++- src/browser/js/Context.zig | 19 +- src/browser/tests/element/html/base.html | 27 ++ src/browser/webapi/element/html/Base.zig | 48 ++ 7 files changed, 686 insertions(+), 68 deletions(-) create mode 100644 src/browser/ImportMap.zig create mode 100644 src/browser/tests/element/html/base.html diff --git a/src/browser/ImportMap.zig b/src/browser/ImportMap.zig new file mode 100644 index 00000000..3066b95d --- /dev/null +++ b/src/browser/ImportMap.zig @@ -0,0 +1,535 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Parsed + + + + + diff --git a/src/browser/webapi/element/html/Base.zig b/src/browser/webapi/element/html/Base.zig index 0c2f2e8a..bb5616c8 100644 --- a/src/browser/webapi/element/html/Base.zig +++ b/src/browser/webapi/element/html/Base.zig @@ -1,4 +1,7 @@ const js = @import("../../../js/js.zig"); +const URL = @import("../../../URL.zig"); +const Frame = @import("../../../Frame.zig"); + const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -14,6 +17,44 @@ pub fn asNode(self: *Base) *Node { return self.asElement().asNode(); } +pub fn getHref(self: *Base, frame: *Frame) ![]const u8 { + const element = self.asElement(); + const href = element.getAttributeSafe(comptime .wrap("href")) orelse return ""; + if (href.len == 0) { + return ""; + } + return URL.resolve(frame.call_arena, frame.url, href, .{}); +} + +pub fn setHref(self: *Base, value: []const u8, frame: *Frame) !void { + const element = self.asElement(); + try element.setAttributeSafe(comptime .wrap("href"), .wrap(value), frame); + + // Per HTML spec, the document's base URL is the href of the FIRST + // element in tree order that has an href attribute — not necessarily this + // one. Re-derive from scratch so that setting href on a non-authoritative + // , or clearing href on the authoritative one, both work correctly. + const node = element.asNode(); + if (!node.isConnected()) { + return; + } + + const owner = node.ownerFrame(frame); + const first = (try owner.document.querySelector(comptime .wrap("base[href]"), owner)) orelse { + owner.base_url = null; + return; + }; + const href = first.getAttributeSafe(comptime .wrap("href")) orelse { + owner.base_url = null; + return; + }; + if (href.len == 0) { + owner.base_url = null; + return; + } + owner.base_url = try URL.resolve(owner.arena, owner.url, href, .{}); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Base); @@ -22,4 +63,11 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; }; + + pub const href = bridge.accessor(Base.getHref, Base.setHref, .{ .ce_reactions = true }); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.Base" { + try testing.htmlRunner("element/html/base.html", .{}); +}