export interface MarkupContext { /** `owner/repo` */ repo: string; ref: string; dir?: string; host?: string; } const ABSOLUTE = /^[a-z][a-z0-9+.-]*:|^\/\//i; export const isAbsoluteUrl = (url: string): boolean => ABSOLUTE.test(url); export const isRepoRelative = (url: string): boolean => url !== "" && !isAbsoluteUrl(url) && !url.startsWith("#"); // `.` and `..` have to collapse here instead of reaching the url const normalize = (path: string): string => { const parts: string[] = []; for (const part of path.split("/")) { if (part === "" || part === ".") continue; if (part === "..") parts.pop(); else parts.push(part); } return parts.join("/"); }; const withinRepo = (path: string, ctx: MarkupContext): string => normalize(path.startsWith("/") ? path : `${ctx.dir ?? ""}/${path}`); const splitSuffix = (url: string): [string, string] => { const at = url.search(/[?#]/); return at === -1 ? [url, ""] : [url.slice(0, at), url.slice(at)]; }; // markdown-it percent-encodes destinations before we ever see them, so only the // ref still needs encoding const repoUrl = (kind: string, url: string, ctx: MarkupContext): string => { const [path, suffix] = splitSuffix(url); if (path === "") return url; const ref = encodeURIComponent(ctx.ref); return `/${ctx.repo}/${kind}/${ref}/${withinRepo(path, ctx)}${suffix}`; }; export const treeUrl = (url: string, ctx: MarkupContext): string => repoUrl("tree", url, ctx); export const rawUrl = (url: string, ctx: MarkupContext): string => repoUrl("raw", url, ctx); export const rawSrcset = (srcset: string, ctx: MarkupContext): string => srcset .split(",") .map((candidate) => { const [url, ...descriptors] = candidate.trim().split(/\s+/); if (!isRepoRelative(url)) return candidate.trim(); return [rawUrl(url, ctx), ...descriptors].join(" "); }) .join(", ");