This repository has no description
1export interface MarkupContext {
2 /** `owner/repo` */
3 repo: string;
4 ref: string;
5 dir?: string;
6 host?: string;
7 camo?: boolean;
8}
9
10const ABSOLUTE = /^[a-z][a-z0-9+.-]*:|^\/\//i;
11
12export const isAbsoluteUrl = (url: string): boolean => ABSOLUTE.test(url);
13
14export const isRepoRelative = (url: string): boolean =>
15 url !== "" && !isAbsoluteUrl(url) && !url.startsWith("#");
16
17// `.` and `..` have to collapse here instead of reaching the url
18const normalize = (path: string): string => {
19 const parts: string[] = [];
20 for (const part of path.split("/")) {
21 if (part === "" || part === ".") continue;
22 if (part === "..") parts.pop();
23 else parts.push(part);
24 }
25 return parts.join("/");
26};
27
28const withinRepo = (path: string, ctx: MarkupContext): string =>
29 normalize(path.startsWith("/") ? path : `${ctx.dir ?? ""}/${path}`);
30
31const splitSuffix = (url: string): [string, string] => {
32 const at = url.search(/[?#]/);
33 return at === -1 ? [url, ""] : [url.slice(0, at), url.slice(at)];
34};
35
36// markdown-it percent-encodes destinations before we ever see them, so only the
37// ref still needs encoding
38const repoUrl = (kind: string, url: string, ctx: MarkupContext): string => {
39 const [path, suffix] = splitSuffix(url);
40 if (path === "") return url;
41 const ref = encodeURIComponent(ctx.ref);
42 return `/${ctx.repo}/${kind}/${ref}/${withinRepo(path, ctx)}${suffix}`;
43};
44
45export const treeUrl = (url: string, ctx: MarkupContext): string => repoUrl("tree", url, ctx);
46
47export const rawUrl = (url: string, ctx: MarkupContext): string => repoUrl("raw", url, ctx);
48
49const hostOf = (url: string): string | null => {
50 try {
51 // the base only matters for protocol relative urls, which have a host anyway
52 return new URL(url, "https://invalid.").host;
53 } catch {
54 return null;
55 }
56};
57
58// camo wants the target hex encoded
59const toHex = (value: string): string =>
60 Array.from(new TextEncoder().encode(value), (byte) => byte.toString(16).padStart(2, "0")).join(
61 ""
62 );
63
64export const mediaUrl = (url: string, ctx: MarkupContext): string => {
65 if (isRepoRelative(url)) return rawUrl(url, ctx);
66 if (!ctx.camo || !isAbsoluteUrl(url) || hostOf(url) === ctx.host) return url;
67 return `/camo/${toHex(url)}`;
68};
69
70export const mediaSrcset = (srcset: string, ctx: MarkupContext): string =>
71 srcset
72 .split(",")
73 .map((candidate) => {
74 const [url, ...descriptors] = candidate.trim().split(/\s+/);
75 return [mediaUrl(url, ctx), ...descriptors].join(" ");
76 })
77 .join(", ");