Vectrum
Sign In
39 lines · 1.4 KB · TypeScript
import path from "node:path/posix";
import ReactMarkdown, { defaultUrlTransform } from "react-markdown";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import remarkGfm from "remark-gfm";
import { encodePath } from "@/lib/format";

/**
 * Renders GitHub-flavored markdown. Inline HTML is allowed but sanitized with
 * GitHub's own allowlist, so untrusted READMEs and issue bodies are safe. When
 * `context` is given, relative links and images resolve against the file's
 * location in the repository.
 */
export function Markdown({
  children,
  context,
}: {
  children: string;
  context?: { base: string; gitRef: string; dir: string };
}) {
  return (
    <div className="markdown">
      <ReactMarkdown
        remarkPlugins={[remarkGfm]}
        rehypePlugins={[rehypeRaw, rehypeSanitize]}
        urlTransform={(url, key) => {
          const safe = defaultUrlTransform(url);
          if (!context || !safe || /^([a-z][a-z0-9+.-]*:|\/\/|#|\/)/i.test(safe)) return safe;
          const [target, hash] = safe.split("#");
          const resolved = path.normalize(path.join(context.dir, target)).replace(/^(\.\.\/)+/, "");
          const kind = key === "src" ? "raw" : "blob";
          return `${context.base}/${kind}/${encodePath(context.gitRef)}/${encodePath(resolved)}${hash ? `#${hash}` : ""}`;
        }}
      >
        {children}
      </ReactMarkdown>
    </div>
  );
}