Files
traceability/src/components/documentation/MarkdownViewer.tsx
2026-01-25 14:22:22 +01:00

214 lines
7.8 KiB
TypeScript

import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import { ScrollArea } from '@/components/ui/scroll-area';
import { MermaidDiagram } from './MermaidDiagram';
import { PlantUMLDiagram } from './PlantUMLDiagram';
interface MarkdownViewerProps {
content: string;
className?: string;
}
export function MarkdownViewer({ content, className = '' }: MarkdownViewerProps) {
return (
<ScrollArea className={`h-[500px] ${className}`}>
<div className="prose prose-sm dark:prose-invert max-w-none p-4">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
components={{
h1: ({ children }) => (
<h1 className="text-2xl font-bold text-foreground mb-4 pb-2 border-b border-border">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-xl font-semibold text-foreground mt-6 mb-3">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-lg font-medium text-foreground mt-4 mb-2">
{children}
</h3>
),
h4: ({ children }) => (
<h4 className="text-base font-medium text-foreground mt-3 mb-2">
{children}
</h4>
),
p: ({ children }) => (
<p className="text-muted-foreground mb-3 leading-relaxed">
{children}
</p>
),
ul: ({ children }) => (
<ul className="list-disc pl-6 space-y-1 mb-4 text-muted-foreground">
{children}
</ul>
),
ol: ({ children }) => (
<ol className="list-decimal pl-6 space-y-1 mb-4 text-muted-foreground">
{children}
</ol>
),
li: ({ children }) => (
<li className="text-muted-foreground pl-1">{children}</li>
),
code: ({ className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : '';
const codeContent = String(children).replace(/\n$/, '');
// Handle Mermaid diagrams
if (language === 'mermaid') {
return <MermaidDiagram chart={codeContent} />;
}
// Handle PlantUML diagrams
if (language === 'plantuml' || language === 'puml') {
return <PlantUMLDiagram code={codeContent} />;
}
// Inline code (no language specified and short)
const isInline = !className && !String(children).includes('\n');
if (isInline) {
return (
<code className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono text-foreground" {...props}>
{children}
</code>
);
}
// Code block
return (
<code className="block bg-muted p-4 rounded-lg text-sm font-mono overflow-x-auto mb-4" {...props}>
{children}
</code>
);
},
pre: ({ children, ...props }) => {
// Check if the child is a Mermaid or PlantUML diagram (already rendered)
const childElement = children as React.ReactElement;
if (childElement?.type === MermaidDiagram || childElement?.type === PlantUMLDiagram) {
return <>{children}</>;
}
return (
<pre className="bg-muted p-4 rounded-lg overflow-x-auto mb-4" {...props}>
{children}
</pre>
);
},
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-primary pl-4 italic text-muted-foreground my-4">
{children}
</blockquote>
),
// Enhanced table support for HTML tables
table: ({ children }) => (
<div className="overflow-x-auto mb-4">
<table className="min-w-full border-collapse border border-border rounded-lg">
{children}
</table>
</div>
),
thead: ({ children }) => (
<thead className="bg-muted">{children}</thead>
),
tbody: ({ children }) => (
<tbody className="divide-y divide-border">{children}</tbody>
),
tr: ({ children }) => (
<tr className="hover:bg-muted/50 transition-colors">{children}</tr>
),
th: ({ children, style }) => (
<th
className="px-4 py-2 text-left font-medium text-foreground border border-border bg-muted"
style={style}
>
{children}
</th>
),
td: ({ children, style }) => (
<td
className="px-4 py-2 text-muted-foreground border border-border"
style={style}
>
{children}
</td>
),
hr: () => <hr className="my-6 border-border" />,
a: ({ href, children }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{children}
</a>
),
strong: ({ children }) => (
<strong className="font-semibold text-foreground">{children}</strong>
),
em: ({ children }) => (
<em className="italic">{children}</em>
),
// Support for definition lists (HTML)
dl: ({ children }) => (
<dl className="mb-4 space-y-2">{children}</dl>
),
dt: ({ children }) => (
<dt className="font-medium text-foreground">{children}</dt>
),
dd: ({ children }) => (
<dd className="ml-4 text-muted-foreground">{children}</dd>
),
// Support for figures and captions
figure: ({ children }) => (
<figure className="my-4">{children}</figure>
),
figcaption: ({ children }) => (
<figcaption className="text-center text-sm text-muted-foreground mt-2">
{children}
</figcaption>
),
// Support for images
img: ({ src, alt, ...props }) => (
<img
src={src}
alt={alt || ''}
className="max-w-full h-auto rounded-lg my-4"
{...props}
/>
),
// Support for details/summary
details: ({ children }) => (
<details className="my-4 border border-border rounded-lg p-4 bg-card">
{children}
</details>
),
summary: ({ children }) => (
<summary className="font-medium cursor-pointer text-foreground hover:text-primary">
{children}
</summary>
),
// Div support for custom HTML blocks
div: ({ className, children, ...props }) => (
<div className={className} {...props}>{children}</div>
),
// Span support
span: ({ className, children, style, ...props }) => (
<span className={className} style={style} {...props}>{children}</span>
),
}}
>
{content}
</ReactMarkdown>
</div>
</ScrollArea>
);
}