/ concept-collection / mdshare
Sign in
concept-collection / mdshare
mdshare / src / Preview.tsx
66 lines · 2.0 KBBlameHistoryRaw
1import { memo, useState, type ReactNode } from 'react'
2import ReactMarkdown from 'react-markdown'
3import remarkGfm from 'remark-gfm'
4import remarkMath from 'remark-math'
5import rehypeKatex from 'rehype-katex'
6import rehypeHighlight from 'rehype-highlight'
7import Mermaid from './Mermaid'
9// Note: no rehype-raw — raw HTML in the markdown is rendered as text. Since
10// any document can arrive via a shared link, HTML passthrough would let a
11// link author run script in the viewer's browser.
13function extractText(node: ReactNode): string {
14 if (typeof node === 'string') return node
15 if (Array.isArray(node)) return node.map(extractText).join('')
16 if (node && typeof node === 'object' && 'props' in node) {
17 return extractText((node.props as { children?: ReactNode }).children)
18 }
19 return ''
22function Pre({ children, ...props }: React.HTMLAttributes<HTMLPreElement>) {
23 const [copied, setCopied] = useState(false)
25 // ```mermaid fences render as a diagram instead of a code block
26 const child = Array.isArray(children) ? children[0] : children
27 if (
28 child &&
29 typeof child === 'object' &&
30 'props' in child &&
31 /\blanguage-mermaid\b/.test(
32 String((child.props as { className?: string }).className ?? ''),
33 )
34 ) {
35 return <Mermaid code={extractText(child).replace(/\n$/, '')} />
36 }
38 const copy = () => {
39 navigator.clipboard.writeText(extractText(children).replace(/\n$/, ''))
40 setCopied(true)
41 setTimeout(() => setCopied(false), 1500)
42 }
44 return (
45 <div className="code-block">
46 <button className="copy-button" onClick={copy} title="Copy code">
47 {copied ? 'copied' : 'copy'}
48 </button>
49 <pre {...props}>{children}</pre>
50 </div>
51 )
54export default memo(function Preview({ markdown }: { markdown: string }) {
55 return (
56 <div className="preview">
57 <ReactMarkdown
58 remarkPlugins={[remarkGfm, remarkMath]}
59 rehypePlugins={[rehypeKatex, rehypeHighlight]}
60 components={{ pre: Pre }}
61 >
62 {markdown}
63 </ReactMarkdown>
64 </div>
65 )
66})
moveopenescclose