From 0d61e05e98f6f020722ee4d43f024c1da348d2a9 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Thu, 14 May 2026 12:41:55 -0500 Subject: feat: add Safari extension for sourcehut deep links --- Shared/HutchDeepLinkURLs.swift | 5 +++ Shared/SourceHutWebDeepLinkMapper.swift | 80 +++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 Shared/SourceHutWebDeepLinkMapper.swift (limited to 'Shared') diff --git a/Shared/HutchDeepLinkURLs.swift b/Shared/HutchDeepLinkURLs.swift index 9b60086..eed678c 100644 --- a/Shared/HutchDeepLinkURLs.swift +++ b/Shared/HutchDeepLinkURLs.swift @@ -5,13 +5,18 @@ enum HutchDeepLinkURL { static let home = URL(string: "hutch://home")! static let emptyHost = URL(string: "hutch://")! static let repositoryGit = URL(string: "hutch://git/~user/repo")! + static let repositoryHg = URL(string: "hutch://hg/~user/repo")! + static let gitOwnerRoot = URL(string: "hutch://git/~owner/")! static let ticket = URL(string: "hutch://todo/~owner/tracker/42")! static let buildJob = URL(string: "hutch://builds/12345")! + static let buildJobWithOwner = URL(string: "hutch://builds/~owner/job/12345")! + static let mailingList = URL(string: "hutch://lists/~owner/list")! static let trackers = URL(string: "hutch://trackers")! static let builds = URL(string: "hutch://builds")! static let repositories = URL(string: "hutch://repositories")! static let status = URL(string: "hutch://status")! static let lookup = URL(string: "hutch://lookup")! + static let userProfile = URL(string: "hutch://lookup/~owner")! static let unknown = URL(string: "hutch://unknown")! static let invalidTicketId = URL(string: "hutch://todo/~owner/tracker/abc")! static let invalidBuildId = URL(string: "hutch://builds/abc")! diff --git a/Shared/SourceHutWebDeepLinkMapper.swift b/Shared/SourceHutWebDeepLinkMapper.swift new file mode 100644 index 0000000..236e32e --- /dev/null +++ b/Shared/SourceHutWebDeepLinkMapper.swift @@ -0,0 +1,80 @@ +import Foundation + +enum SourceHutWebDeepLinkMapper { + static let supportedHosts: Set = [ + "git.sr.ht", + "hg.sr.ht", + "todo.sr.ht", + "builds.sr.ht", + "lists.sr.ht", + "meta.sr.ht", + "sr.ht", + ] + + static func deepLink(for webURL: URL) -> URL? { + guard let components = URLComponents(url: webURL, resolvingAgainstBaseURL: false), + components.scheme == "https", + let host = components.host?.lowercased(), + supportedHosts.contains(host) + else { + return nil + } + + let path = normalizedPercentEncodedPath(from: components.percentEncodedPath) + let deepLinkPath = normalizedDeepLinkPath(for: host, path: path) + let service = deepLinkService(for: host, path: deepLinkPath) + var deepLinkComponents = URLComponents() + deepLinkComponents.scheme = "hutch" + deepLinkComponents.host = service + + if !deepLinkPath.isEmpty { + deepLinkComponents.percentEncodedPath = "/" + deepLinkPath + } + deepLinkComponents.percentEncodedQuery = components.percentEncodedQuery + deepLinkComponents.percentEncodedFragment = components.percentEncodedFragment + return deepLinkComponents.url + } + + private static func deepLinkService(for host: String, path: String) -> String { + if isOwnerRootPath(path) { + return "lookup" + } + + switch host { + case "git.sr.ht": + return "git" + case "hg.sr.ht": + return "hg" + case "todo.sr.ht": + return "todo" + case "builds.sr.ht": + return "builds" + case "lists.sr.ht": + return "lists" + default: + return "lookup" + } + } + + private static func isOwnerRootPath(_ path: String) -> Bool { + let components = path.split(separator: "/", omittingEmptySubsequences: true) + return components.count == 1 && components[0].hasPrefix("~") + } + + private static func normalizedDeepLinkPath(for host: String, path: String) -> String { + let components = path.split(separator: "/", omittingEmptySubsequences: true) + if host == "sr.ht", + components.count == 2, + components[0] == "projects", + components[1].hasPrefix("~") { + return String(components[1]) + } + return path + } + + private static func normalizedPercentEncodedPath(from path: String) -> String { + path + .split(separator: "/", omittingEmptySubsequences: true) + .joined(separator: "/") + } +} -- cgit v1.2.3