Skip to content
Source
Sign inSign up

pullRequestAutolinks.ts

73 lines2.5 KB

1import type { ReferenceAutolink } from "@t3tools/client-runtime/source-control-clients";
2import remarkGfm from "remark-gfm";
3import remarkParse from "remark-parse";
4import { unified } from "unified";
5​
6type MarkdownNode = {
7 readonly type: string;
8 readonly position?:
9 | {
10 readonly start: { readonly offset?: number | undefined };
11 readonly end: { readonly offset?: number | undefined };
12 }
13 | undefined;
14 readonly children?: readonly MarkdownNode[] | undefined;
15};
16​
17const parser = unified().use(remarkParse).use(remarkGfm);
18const UNLINKED_TYPES = new Set([
19 "link",
20 "linkReference",
21 "image",
22 "imageReference",
23 "code",
24 "inlineCode",
25 "html",
26 "definition",
27]);
28​
29/**
30 * Rewrites the references `autolinks` finds in the markdown's prose as markdown links, because the
31 * native renderer takes markdown text. Text is searched as written, escapes included.
32 */
33export function linkReferenceAutolinks(
34 markdown: string,
35 autolinks: ((text: string) => ReadonlyArray<ReferenceAutolink>) | null,
36): string {
37 if (autolinks === null) return markdown;
38 const edits: { start: number; end: number; value: string }[] = [];
39 const visit = (node: MarkdownNode) => {
40 if (UNLINKED_TYPES.has(node.type)) return;
41 if (node.type === "text") {
42 const start = node.position?.start.offset;
43 const end = node.position?.end.offset;
44 if (start === undefined || end === undefined) return;
45 for (const link of autolinks(markdown.slice(start, end))) {
46 edits.push({
47 start: start + link.index,
48 end: start + link.index + link.text.length,
49 value: `[${link.label}](${link.url})`,
50 });
51 }
52 } else if (node.children !== undefined) {
53 let protectedHtmlDepth = 0;
54 for (const child of node.children) {
55 if (child.type === "html") {
56 const start = child.position?.start.offset;
57 const end = child.position?.end.offset;
58 const tag = start === undefined || end === undefined ? "" : markdown.slice(start, end);
59 for (const match of tag.matchAll(/<(\/)?(?:a|code|pre|script|style)(?=[\s>])[^>]*>/giu)) {
60 protectedHtmlDepth = Math.max(0, protectedHtmlDepth + (match[1] ? -1 : 1));
61 }
62 } else if (protectedHtmlDepth === 0) visit(child);
63 }
64 }
65 };
66 visit(parser.parse(markdown));
67 let linked = markdown;
68 for (let index = edits.length - 1; index >= 0; index--) {
69 const edit = edits[index]!;
70 linked = linked.slice(0, edit.start) + edit.value + linked.slice(edit.end);
71 }
72 return linked;