// app/markdown-renderer.tsx
"use client";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import type { MarkdownRendererProps } from "veryfront/markdown";
const LINK_REL = "noopener noreferrer nofollow";
/** Allow http(s), mailto, and relative URLs; drop every other scheme. */
function sanitizeUrl(url: string): string {
// Browsers ignore ASCII spaces and control characters while parsing a
// scheme, so `java	script:` and ` data:` navigate exactly like the bare
// scheme does. Match against a copy with those characters removed so an
// obfuscated scheme is recognised and dropped.
const probe = Array.from(url)
.filter((char) => char > " " && char !== "\u007f")
.join("");
if (/^(https?|mailto):/i.test(probe)) return url;
return /^[a-z][a-z0-9+.-]*:/i.test(probe) ? "" : url;
}
export function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element {
return (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
urlTransform={sanitizeUrl}
components={{
a: ({ href, children }) => (
<a href={href || undefined} target="_blank" rel={LINK_REL}>
{children}
</a>
),
// Auto-loading images would let an injected answer beacon to arbitrary
// hosts. Render inert text, not an anchor: Markdown allows a linked
// image (`[](href)`), and an anchor here would nest inside
// that link, which is invalid HTML and breaks hydration.
img: ({ src, alt }) => (
<span title={typeof src === "string" ? src : undefined}>{alt || "image"}</span>
),
}}
>
{source}
</ReactMarkdown>
);
}