feat(MarkdownEditor): integrate Mermaid support for diagram rendering in Markdown
- Added MermaidRenderer component to handle Mermaid diagrams within Markdown content. - Enhanced preformatted code block handling to detect and render Mermaid syntax. - Updated package.json and package-lock.json to include Mermaid dependency for diagram support.
This commit is contained in:
1086
package-lock.json
generated
1086
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,7 @@
|
||||
"emoji-mart": "^5.6.0",
|
||||
"emoji-regex": "^10.5.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"mermaid": "^11.12.0",
|
||||
"next": "15.5.3",
|
||||
"next-auth": "^4.24.11",
|
||||
"prisma": "^6.16.1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import React from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
@@ -9,6 +10,7 @@ import { Eye, EyeOff, Edit3, X, CheckSquare2 } from 'lucide-react';
|
||||
import { TagInput } from '@/components/ui/TagInput';
|
||||
import { TagDisplay } from '@/components/ui/TagDisplay';
|
||||
import { TaskSelectorWithData } from '@/components/shared/TaskSelectorWithData';
|
||||
import { MermaidRenderer } from '@/components/ui/MermaidRenderer';
|
||||
import { Tag, Task } from '@/lib/types';
|
||||
|
||||
interface MarkdownEditorProps {
|
||||
@@ -514,11 +516,50 @@ export function MarkdownEditor({
|
||||
}
|
||||
return <code className={className}>{children}</code>;
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
pre: ({ children }) => {
|
||||
// Check if this is a mermaid code block
|
||||
let codeElement: string | null = null;
|
||||
let className = '';
|
||||
|
||||
if (React.isValidElement(children)) {
|
||||
const props = children.props as {
|
||||
children?: string;
|
||||
className?: string;
|
||||
};
|
||||
codeElement = props?.children || null;
|
||||
className = props?.className || '';
|
||||
}
|
||||
|
||||
const isMermaid =
|
||||
className.includes('language-mermaid') ||
|
||||
(typeof codeElement === 'string' &&
|
||||
(codeElement.trim().startsWith('graph') ||
|
||||
codeElement.trim().startsWith('flowchart') ||
|
||||
codeElement.trim().startsWith('sequenceDiagram') ||
|
||||
codeElement.trim().startsWith('gantt') ||
|
||||
codeElement.trim().startsWith('pie') ||
|
||||
codeElement.trim().startsWith('gitgraph') ||
|
||||
codeElement.trim().startsWith('journey') ||
|
||||
codeElement.trim().startsWith('stateDiagram') ||
|
||||
codeElement.trim().startsWith('classDiagram') ||
|
||||
codeElement.trim().startsWith('erDiagram') ||
|
||||
codeElement.trim().startsWith('mindmap') ||
|
||||
codeElement.trim().startsWith('timeline')));
|
||||
|
||||
if (isMermaid && typeof codeElement === 'string') {
|
||||
return (
|
||||
<div className="my-6">
|
||||
<MermaidRenderer chart={codeElement} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<pre className="bg-[var(--card)]/60 border border-[var(--border)]/60 rounded-lg p-4 overflow-x-auto backdrop-blur-sm">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
);
|
||||
},
|
||||
a: ({ children, href }) => (
|
||||
<a
|
||||
href={href}
|
||||
@@ -701,11 +742,52 @@ export function MarkdownEditor({
|
||||
}
|
||||
return <code className={className}>{children}</code>;
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
pre: ({ children }) => {
|
||||
// Check if this is a mermaid code block
|
||||
let codeElement: string | null = null;
|
||||
let className = '';
|
||||
|
||||
if (React.isValidElement(children)) {
|
||||
const props = children.props as {
|
||||
children?: string;
|
||||
className?: string;
|
||||
};
|
||||
codeElement = props?.children || null;
|
||||
className = props?.className || '';
|
||||
}
|
||||
|
||||
const isMermaid =
|
||||
className.includes('language-mermaid') ||
|
||||
(typeof codeElement === 'string' &&
|
||||
(codeElement.trim().startsWith('graph') ||
|
||||
codeElement.trim().startsWith('flowchart') ||
|
||||
codeElement
|
||||
.trim()
|
||||
.startsWith('sequenceDiagram') ||
|
||||
codeElement.trim().startsWith('gantt') ||
|
||||
codeElement.trim().startsWith('pie') ||
|
||||
codeElement.trim().startsWith('gitgraph') ||
|
||||
codeElement.trim().startsWith('journey') ||
|
||||
codeElement.trim().startsWith('stateDiagram') ||
|
||||
codeElement.trim().startsWith('classDiagram') ||
|
||||
codeElement.trim().startsWith('erDiagram') ||
|
||||
codeElement.trim().startsWith('mindmap') ||
|
||||
codeElement.trim().startsWith('timeline')));
|
||||
|
||||
if (isMermaid && typeof codeElement === 'string') {
|
||||
return (
|
||||
<div className="my-6">
|
||||
<MermaidRenderer chart={codeElement} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<pre className="bg-[var(--card)]/60 border border-[var(--border)]/60 rounded-lg p-4 overflow-x-auto backdrop-blur-sm">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
);
|
||||
},
|
||||
a: ({ children, href }) => (
|
||||
<a
|
||||
href={href}
|
||||
|
||||
240
src/components/ui/MermaidRenderer.tsx
Normal file
240
src/components/ui/MermaidRenderer.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import mermaid from 'mermaid';
|
||||
|
||||
interface MermaidRendererProps {
|
||||
chart: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function MermaidRenderer({
|
||||
chart,
|
||||
className = '',
|
||||
}: MermaidRendererProps) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const renderChart = async () => {
|
||||
if (!chartRef.current || !chart) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Clear previous content
|
||||
chartRef.current.innerHTML = '';
|
||||
|
||||
// Generate unique ID for this chart
|
||||
const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// Always reinitialize to pick up theme changes
|
||||
// Get computed CSS variable values
|
||||
const getCSSVariable = (variable: string): string => {
|
||||
return (
|
||||
getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(variable)
|
||||
.trim() || '#000000'
|
||||
);
|
||||
};
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'base',
|
||||
themeVariables: {
|
||||
// Use computed CSS variable values
|
||||
primaryColor: getCSSVariable('--primary'),
|
||||
primaryTextColor: getCSSVariable('--foreground'),
|
||||
primaryBorderColor: getCSSVariable('--border'),
|
||||
lineColor: getCSSVariable('--foreground'),
|
||||
sectionBkgColor: getCSSVariable('--card'),
|
||||
altSectionBkgColor: getCSSVariable('--card-hover'),
|
||||
gridColor: getCSSVariable('--border'),
|
||||
secondaryColor: getCSSVariable('--accent'),
|
||||
tertiaryColor: getCSSVariable('--muted'),
|
||||
background: getCSSVariable('--background'),
|
||||
mainBkg: getCSSVariable('--card'),
|
||||
secondBkg: getCSSVariable('--card-hover'),
|
||||
tertiaryBkg: getCSSVariable('--muted'),
|
||||
// Additional theme variables for better integration
|
||||
nodeBkg: getCSSVariable('--card'),
|
||||
nodeBorder: getCSSVariable('--border'),
|
||||
clusterBkg: getCSSVariable('--card-hover'),
|
||||
clusterBorder: getCSSVariable('--border'),
|
||||
defaultLinkColor: getCSSVariable('--primary'),
|
||||
titleColor: getCSSVariable('--foreground'),
|
||||
edgeLabelBackground: getCSSVariable('--card'),
|
||||
edgeLabelColor: getCSSVariable('--foreground'),
|
||||
// Sequence diagram specific
|
||||
actorBkg: getCSSVariable('--card'),
|
||||
actorBorder: getCSSVariable('--border'),
|
||||
actorTextColor: getCSSVariable('--foreground'),
|
||||
actorLineColor: getCSSVariable('--foreground'),
|
||||
signalColor: getCSSVariable('--primary'),
|
||||
signalTextColor: getCSSVariable('--foreground'),
|
||||
labelBoxBkgColor: getCSSVariable('--card'),
|
||||
labelBoxBorderColor: getCSSVariable('--border'),
|
||||
labelTextColor: getCSSVariable('--foreground'),
|
||||
loopTextColor: getCSSVariable('--foreground'),
|
||||
activationBkgColor: getCSSVariable('--primary'),
|
||||
activationBorderColor: getCSSVariable('--primary'),
|
||||
// Gantt specific
|
||||
section0: getCSSVariable('--primary'),
|
||||
section1: getCSSVariable('--accent'),
|
||||
section2: getCSSVariable('--success'),
|
||||
section3: getCSSVariable('--purple'),
|
||||
altSection: getCSSVariable('--muted'),
|
||||
gridColor: getCSSVariable('--border'),
|
||||
todayLineColor: getCSSVariable('--destructive'),
|
||||
// Pie chart specific
|
||||
pie1: getCSSVariable('--primary'),
|
||||
pie2: getCSSVariable('--accent'),
|
||||
pie3: getCSSVariable('--success'),
|
||||
pie4: getCSSVariable('--purple'),
|
||||
pie5: getCSSVariable('--yellow'),
|
||||
pie6: getCSSVariable('--blue'),
|
||||
pie7: getCSSVariable('--green'),
|
||||
pieTitleTextSize: '16px',
|
||||
pieTitleTextColor: getCSSVariable('--foreground'),
|
||||
pieSectionTextSize: '14px',
|
||||
pieSectionTextColor: getCSSVariable('--foreground'),
|
||||
pieLegendTextSize: '12px',
|
||||
pieLegendTextColor: getCSSVariable('--muted-foreground'),
|
||||
pieStrokeColor: getCSSVariable('--border'),
|
||||
pieStrokeWidth: '2px',
|
||||
pieOuterStrokeWidth: '2px',
|
||||
pieOuterStrokeColor: getCSSVariable('--border'),
|
||||
pieOpacity: '0.8',
|
||||
},
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 14,
|
||||
flowchart: {
|
||||
useMaxWidth: true,
|
||||
htmlLabels: true,
|
||||
curve: 'basis',
|
||||
},
|
||||
sequence: {
|
||||
useMaxWidth: true,
|
||||
diagramMarginX: 50,
|
||||
diagramMarginY: 10,
|
||||
actorMargin: 50,
|
||||
width: 150,
|
||||
height: 65,
|
||||
boxMargin: 10,
|
||||
boxTextMargin: 5,
|
||||
noteMargin: 10,
|
||||
messageMargin: 35,
|
||||
messageAlign: 'center',
|
||||
mirrorActors: true,
|
||||
bottomMarginAdj: 1,
|
||||
useMaxWidth: true,
|
||||
rightAngles: false,
|
||||
showSequenceNumbers: false,
|
||||
},
|
||||
gantt: {
|
||||
useMaxWidth: true,
|
||||
leftPadding: 75,
|
||||
gridLineStartPadding: 35,
|
||||
fontSize: 11,
|
||||
fontFamily: 'inherit',
|
||||
sectionFontSize: 24,
|
||||
numberSectionStyles: 4,
|
||||
},
|
||||
pie: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
journey: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
gitgraph: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
state: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
class: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
er: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
mindmap: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
timeline: {
|
||||
useMaxWidth: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Render the chart
|
||||
const { svg } = await mermaid.render(id, chart);
|
||||
|
||||
if (isMounted && chartRef.current) {
|
||||
chartRef.current.innerHTML = svg;
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isMounted) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: 'Erreur lors du rendu du diagramme'
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderChart();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [chart]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
className={`p-4 border border-[var(--destructive)]/20 bg-[var(--destructive)]/5 rounded-lg ${className}`}
|
||||
>
|
||||
<div className="text-[var(--destructive)] text-sm font-medium mb-2">
|
||||
Erreur de rendu Mermaid
|
||||
</div>
|
||||
<div className="text-[var(--muted-foreground)] text-xs font-mono bg-[var(--card)] p-2 rounded border">
|
||||
{error}
|
||||
</div>
|
||||
<details className="mt-2">
|
||||
<summary className="text-xs text-[var(--muted-foreground)] cursor-pointer">
|
||||
Code source
|
||||
</summary>
|
||||
<pre className="text-xs text-[var(--muted-foreground)] mt-1 p-2 bg-[var(--card)] rounded border overflow-x-auto">
|
||||
{chart}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-[var(--background)]/80 backdrop-blur-sm rounded-lg">
|
||||
<div className="text-[var(--muted-foreground)] text-sm">
|
||||
Rendu du diagramme...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={chartRef}
|
||||
className="mermaid-chart rounded-lg border border-[var(--border)]/60 bg-[var(--card)]/30 p-4 backdrop-blur-sm"
|
||||
style={{
|
||||
minHeight: isLoading ? '100px' : 'auto',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user