kdnk_obsidian-automatic-linker/src/replace-urls/github.ts
Kodai Nakamura 621b15a7a1 refactor(replace-urls): centralize prose-aware URL formatting
Why:
- URL formatting orchestration was split across formatting-run and the GitHub adapter, which made adapter ordering harder to reason about.
- Task 4 added prose segmentation, and URL formatting needed to use it so protected Markdown stays untouched while still supporting linear:// URLs.

What:
- add a dedicated url-formatting module that applies GitHub, Jira, and Linear adapters in one prose-aware pass
- route formatting-run document/body URL formatting through the central formatter and remove cross-adapter orchestration from github.ts
- extend markdown segmentation and compatibility tests to cover protected angle-bracket autolinks and linear:// matching
2026-06-22 03:22:28 +09:00

124 lines
3.3 KiB
TypeScript

import { AutomaticLinkerSettings } from "../settings/settings-info"
type GitHubURLInfo = {
owner: string
repository: string
type?: "pull" | "issues"
id?: string
}
/**
* Format a GitHub URL by normalizing the format and converting to Obsidian link format:
* [[github_id/repository/{pull or issue}/{id}]] [🔗](url)
* @param url The URL to format
* @param settings Plugin settings
* @returns The formatted URL in Obsidian link format
*/
export function formatGitHubURL(
url: string,
settings: AutomaticLinkerSettings,
): string {
try {
const githubURL = new URL(url)
// Check if it's a GitHub URL (including Enterprise)
if (!isGitHubURL(githubURL, settings.githubEnterpriseURLs)) {
return url
}
const urlInfo = parseGitHubURL(githubURL)
if (!urlInfo) {
return url
}
const cleanURL = getCleanURL(githubURL, urlInfo)
return formatToObsidianLink(urlInfo, cleanURL)
}
catch {
// If URL is invalid, return original string
return url
}
}
/**
* Parse GitHub URL into its components
*/
function parseGitHubURL(url: URL): GitHubURLInfo | null {
const parts = url.pathname.split("/").filter(Boolean)
if (parts.length < 2) {
return null
}
const [owner, repository, type, id] = parts
const urlInfo: GitHubURLInfo = {
owner,
repository,
}
if (isPullRequestOrIssueURL(url)) {
urlInfo.type = type as "pull" | "issues"
urlInfo.id = id
}
return urlInfo
}
/**
* Get clean URL without query parameters and trailing slashes
*/
function getCleanURL(url: URL, urlInfo: GitHubURLInfo): string {
if (urlInfo.type && urlInfo.id) {
const basePath = `/${urlInfo.owner}/${urlInfo.repository}/${urlInfo.type}/${urlInfo.id}`
return `${url.origin}${basePath}`
}
return `${url.origin}/${urlInfo.owner}/${urlInfo.repository}`.replace(
/\/$/,
"",
)
}
/**
* Format URL info into Obsidian link format
*/
function formatToObsidianLink(
urlInfo: GitHubURLInfo,
cleanURL: string,
): string {
const url = new URL(cleanURL)
const isEnterpriseURL = url.hostname !== "github.com"
const prefix = isEnterpriseURL ? "ghe" : "github"
let wikiLink = `[[${prefix}/${urlInfo.owner}/${urlInfo.repository}`
if (urlInfo.type && urlInfo.id) {
wikiLink += `/${urlInfo.type}/${urlInfo.id}`
}
wikiLink += `]] [🔗](${cleanURL})`
return wikiLink
}
/**
* Check if the URL is a GitHub URL (including Enterprise)
*/
function isGitHubURL(url: URL, enterpriseURLs: string[]): boolean {
// Check if it's github.com
if (url.hostname === "github.com") {
return true
}
// Check if it matches any of the configured enterprise URLs
return enterpriseURLs.some((enterpriseURL) => {
// Remove any protocol and trailing slashes from the enterprise URL
const cleanEnterpriseURL = enterpriseURL
.replace(/^https?:\/\//, "")
.replace(/\/$/, "")
return url.hostname === cleanEnterpriseURL
})
}
/**
* Check if the URL is a pull request or issue URL
*/
function isPullRequestOrIssueURL(url: URL): boolean {
const path = url.pathname.toLowerCase()
return path.includes("/pull/") || path.includes("/issues/")
}