export interface MarkupContext {
/** `owner/repo` */
repo: string;
ref: string;
dir?: string;
host?: string;
camo?: boolean;
sourceLines?: boolean;
collapseCode?: boolean;
}
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);
const hostOf = (url: string): string | null => {
try {
// the base only matters for protocol relative urls, which have a host anyway
return new URL(url, "https://invalid.").host;
} catch {
return null;
}
};
// camo wants the target hex encoded
export const toHex = (value: string): string =>
Array.from(new TextEncoder().encode(value), (byte) => byte.toString(16).padStart(2, "0")).join(
""
);
// TODO(post-1.0): remove this once tangled.org serves svfe.
const BLOB_PREFIX = "https://tangled.org/blob/";
let blobUrl = "https://tangled.org";
export const setBlobUrl = (url: string) => {
blobUrl = url;
};
export const getBlobUrl = (): string => blobUrl;
export const blobLink = (owner: string, cid: string): string => `${blobUrl}/blob/${owner}/${cid}`;
export const mediaUrl = (url: string, ctx: MarkupContext): string => {
if (blobUrl !== "https://tangled.org" && url.startsWith(BLOB_PREFIX))
url = `${blobUrl}/blob/${url.slice(BLOB_PREFIX.length)}`;
if (isRepoRelative(url)) return rawUrl(url, ctx);
if (!ctx.camo || !isAbsoluteUrl(url) || hostOf(url) === ctx.host) return url;
return `/camo/${toHex(url)}`;
};
export const mediaSrcset = (srcset: string, ctx: MarkupContext): string =>
srcset
.split(",")
.map((candidate) => {
const [url, ...descriptors] = candidate.trim().split(/\s+/);
return [mediaUrl(url, ctx), ...descriptors].join(" ");
})
.join(", ");