Repository Public MIT
1import ReactMarkdown, { defaultUrlTransform } from "react-markdown";2import rehypeHighlight from "rehype-highlight";3import remarkGfm from "remark-gfm";4import { CopyButton } from "./copy-button";56function text(node: React.ReactNode): string {7 if (typeof node === "string" || typeof node === "number") return String(node);8 if (Array.isArray(node)) return node.map(text).join("");9 if (node && typeof node === "object" && "props" in node) return text((node as { props: { children?: React.ReactNode } }).props.children);10 return "";11}12const slug = (children: React.ReactNode) => text(children).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || undefined;13const LANG: Record<string, string> = { sh: "shell", bash: "shell", zsh: "shell", shell: "shell", js: "javascript", mjs: "javascript", ts: "typescript", py: "python", yml: "yaml", md: "markdown" };1415/** Fenced code: a frame with the language and a copy button. The code itself stays plain text. */16function Pre({ children }: { children?: React.ReactNode }) {17 const child = Array.isArray(children) ? children[0] : children;18 const cls = (child && typeof child === "object" && "props" in child ? (child as { props: { className?: string } }).props.className : "") ?? "";19 const lang = /language-([\w+-]+)/.exec(cls)?.[1];20 const raw = text(children).replace(/\n$/, "");21 return (22 <div className="code-frame">23 <div className="code-head">24 <span className="font-mono text-[11px] text-faint">{lang ? LANG[lang] ?? lang : "text"}</span>25 <CopyButton text={raw} />26 </div>27 <pre tabIndex={0}>{children}</pre>28 </div>29 );30}3132/**33 * Safe Markdown. react-markdown never renders raw HTML, URLs go through its protocol allow-list,34 * images are dropped (no third-party requests from untrusted READMEs), and untrusted links are35 * marked nofollow/ugc and cannot use relative targets.36 */37export function Markdown({ source, trusted = false }: { source: string; trusted?: boolean }) {38 return (39 <div className="prose-registry">40 <ReactMarkdown41 remarkPlugins={[remarkGfm]}42 rehypePlugins={[[rehypeHighlight, { detect: false, ignoreMissing: true }]]}43 disallowedElements={["img"]}44 urlTransform={(url) => {45 const u = defaultUrlTransform(url);46 if (trusted) return u;47 return /^(https?:|mailto:|#)/i.test(u) ? u : "";48 }}49 components={{50 a: ({ href, children }) => {51 if (!href) return <span>{children}</span>;52 const external = /^https?:/i.test(href);53 return (54 <a href={href} {...(external ? { rel: trusted ? "noopener" : "nofollow ugc noopener noreferrer", target: "_blank" } : {})}>55 {children}56 </a>57 );58 },59 pre: Pre,60 table: ({ children }) => (61 <div className="table-wrap" tabIndex={0} role="group" aria-label="Table">62 <table>{children}</table>63 </div>64 ),65 // Untrusted documents never own the page's h1: their headings are demoted one level.66 ...(trusted67 ? {68 h2: ({ children }: { children?: React.ReactNode }) => <h2 id={slug(children)}>{children}</h2>,69 h3: ({ children }: { children?: React.ReactNode }) => <h3 id={slug(children)}>{children}</h3>,70 }71 : {72 h1: ({ children }: { children?: React.ReactNode }) => <h2>{children}</h2>,73 h2: ({ children }: { children?: React.ReactNode }) => <h3>{children}</h3>,74 h3: ({ children }: { children?: React.ReactNode }) => <h4>{children}</h4>,75 h4: ({ children }: { children?: React.ReactNode }) => <h5>{children}</h5>,76 }),77 }}78 >79 {source}80 </ReactMarkdown>81 </div>82 );83}84