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-mart": "^5.6.0",
|
||||||
"emoji-regex": "^10.5.0",
|
"emoji-regex": "^10.5.0",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
|
"mermaid": "^11.12.0",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"prisma": "^6.16.1",
|
"prisma": "^6.16.1",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import React from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import rehypeHighlight from 'rehype-highlight';
|
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 { TagInput } from '@/components/ui/TagInput';
|
||||||
import { TagDisplay } from '@/components/ui/TagDisplay';
|
import { TagDisplay } from '@/components/ui/TagDisplay';
|
||||||
import { TaskSelectorWithData } from '@/components/shared/TaskSelectorWithData';
|
import { TaskSelectorWithData } from '@/components/shared/TaskSelectorWithData';
|
||||||
|
import { MermaidRenderer } from '@/components/ui/MermaidRenderer';
|
||||||
import { Tag, Task } from '@/lib/types';
|
import { Tag, Task } from '@/lib/types';
|
||||||
|
|
||||||
interface MarkdownEditorProps {
|
interface MarkdownEditorProps {
|
||||||
@@ -514,11 +516,50 @@ export function MarkdownEditor({
|
|||||||
}
|
}
|
||||||
return <code className={className}>{children}</code>;
|
return <code className={className}>{children}</code>;
|
||||||
},
|
},
|
||||||
pre: ({ children }) => (
|
pre: ({ children }) => {
|
||||||
<pre className="bg-[var(--card)]/60 border border-[var(--border)]/60 rounded-lg p-4 overflow-x-auto backdrop-blur-sm">
|
// Check if this is a mermaid code block
|
||||||
{children}
|
let codeElement: string | null = null;
|
||||||
</pre>
|
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: ({ children, href }) => (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
@@ -701,11 +742,52 @@ export function MarkdownEditor({
|
|||||||
}
|
}
|
||||||
return <code className={className}>{children}</code>;
|
return <code className={className}>{children}</code>;
|
||||||
},
|
},
|
||||||
pre: ({ children }) => (
|
pre: ({ children }) => {
|
||||||
<pre className="bg-[var(--card)]/60 border border-[var(--border)]/60 rounded-lg p-4 overflow-x-auto backdrop-blur-sm">
|
// Check if this is a mermaid code block
|
||||||
{children}
|
let codeElement: string | null = null;
|
||||||
</pre>
|
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: ({ children, href }) => (
|
||||||
<a
|
<a
|
||||||
href={href}
|
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