~james/core

core/web/src/lib/markup/paths.ts -rw-r--r-- 2.9 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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(", ");