import MarkdownIt from "markdown-it"; import type { MarkdownIt as MarkdownRenderer, StateCore } from "markdown-it"; import { highlight } from "sugar-high"; import { linkTextReferences } from "./references"; export type MarkdownContext = { owner: string; repo: string; ref: string; /** Directory of the document inside the repo, used to resolve relative links. */ basePath: string; }; const ATTACHMENT_PREFIX = "/attachments/"; const ABSOLUTE_URL = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i; function createRenderer(): MarkdownRenderer { const md = new MarkdownIt({ html: false, linkify: true, typographer: false, highlight: (code) => `
${highlight(code)}`,
});
md.validateLink = (url) =>
!/^(javascript|vbscript|data):/i.test(url.trim()) || /^data:image\/(png|gif|jpe?g|webp);/i.test(url.trim());
return md;
}
const renderer = createRenderer();
function resolveRepoPath(basePath: string, target: string): string {
const parts = (target.startsWith("/") ? [] : basePath.split("/").filter(Boolean)).concat(target.split("/"));
const out: string[] = [];
for (const part of parts) {
if (!part || part === ".") continue;
if (part === "..") out.pop();
else out.push(part);
}
return out.join("/");
}
function rewriteRelative(env: MarkdownContext | undefined, url: string, kind: "link" | "image"): string {
if (!env || !url || ABSOLUTE_URL.test(url) || url.startsWith(ATTACHMENT_PREFIX)) return url;
const [pathPart, hash] = url.split("#", 2);
const resolved = resolveRepoPath(env.basePath, pathPart);
const mode = kind === "image" ? "raw" : "blob";
return `/${env.owner}/${env.repo}/${mode}/${env.ref}/${resolved}${hash ? `#${hash}` : ""}`;
}
let activeContext: MarkdownContext | undefined;
renderer.core.ruler.push("stig_links", (state: StateCore) => {
const context = activeContext;
for (const token of state.tokens) {
for (const child of token.children ?? []) {
if (child.type === "link_open") {
const raw = child.attrGet("href");
const href = raw === null ? null : String(raw);
if (href) child.attrSet("href", rewriteRelative(context, href, "link"));
if (href && /^https?:/i.test(href)) child.attrSet("rel", "nofollow noopener noreferrer");
}
if (child.type === "image") {
const src = child.attrGet("src");
if (src !== null) child.attrSet("src", rewriteRelative(context, String(src), "image"));
}
}
}
});
const SKIP_TAGS = /^<(\/?)(code|pre|a)\b/i;
function linkReferences(html: string, context: Pick