import React from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; interface MarkdownBlockProps { content: string; } import { Components } from "react-markdown"; const SafeLink: Components["a"] = ({ href, children, ...rest }) => { const rawHref = typeof href === "string" ? href.trim() : ""; const normalized = rawHref.toLowerCase(); const isAllowed = normalized.startsWith("https://") || normalized.startsWith("mailto:"); if (!isAllowed) { return {children}; } return ( {children} ); }; const SanitizedImage: Components["img"] = ({ alt }) => { if (typeof alt === "string" && alt.trim().length > 0) { return {alt}; } return null; }; // Minimal remark plugin to convert raw HTML
nodes to mdast hard breaks. // This preserves line breaks inside table cells without enabling arbitrary HTML. function remarkHtmlBrToBreak() { return (tree: any) => { const walk = (node: any) => { if (!node) return; if (node.type === 'html' && typeof node.value === 'string') { const raw = node.value.trim().toLowerCase(); if (/^$/.test(raw)) { node.type = 'break'; delete node.value; } else { // Render other raw HTML as plain text to avoid showing tags as elements node.type = 'text'; // keep original text } } if (Array.isArray(node.children)) { for (const child of node.children) walk(child); } }; walk(tree); }; } const MarkdownBlock: React.FC = React.memo(({ content }) => { const markdown = content || ''; return ( {markdown} ); }); export default MarkdownBlock;