Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 3 additions & 142 deletions components/MDX/MDX.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { MDXProvider as CoreMDXProvider } from '@mdx-js/react';
import mermaid from 'mermaid';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import React, { useEffect, useId, useState } from 'react';
import React, { useId } from 'react';
import {
TwitterDMButton,
TwitterFollowButton,
Expand Down Expand Up @@ -41,146 +41,7 @@ import Sponsors from '../sponsors/PlatinumSponsors';
import Warning from '../Warning';
import { Table, TableBody, TableCell, TableHeader, TableRow, Thead } from './MDXTable';

type MermaidTheme = 'light' | 'dark';

const MERMAID_THEME_VARIABLES: Record<MermaidTheme, Record<string, string>> = {
light: {
primaryColor: '#EDFAFF',
primaryBorderColor: '#47BCEE',
secondaryColor: '#F4EFFC',
secondaryBorderColor: '#875AE2',
fontFamily: 'Inter, sans-serif',
fontSize: '18px',
primaryTextColor: '#242929',
tertiaryColor: '#F7F9FA',
tertiaryBorderColor: '#BFC6C7',
lineColor: '#BFC6C7',
mainBkg: '#EDFAFF',
secondBkg: '#F4EFFC',
tertiaryBkg: '#F7F9FA',
clusterBkg: '#F7F9FA',
clusterBorder: '#BFC6C7',
edgeLabelBackground: '#FFFFFF'
},
dark: {
primaryColor: '#1E293B',
primaryBorderColor: '#38BDF8',
secondaryColor: '#2E2459',
secondaryBorderColor: '#A87EFC',
fontFamily: 'Inter, sans-serif',
fontSize: '18px',
primaryTextColor: '#F8FAFC',
tertiaryColor: '#121825',
tertiaryBorderColor: '#475569',
lineColor: '#94A3B8',
mainBkg: '#1E293B',
secondBkg: '#2E2459',
tertiaryBkg: '#121825',
clusterBkg: '#121825',
clusterBorder: '#475569',
edgeLabelBackground: '#1E293B'
}
};

// Cache the theme Mermaid was initialized with across client-side page transitions.
let initializedMermaidTheme: MermaidTheme | null = null;

/**
* @description Returns the Mermaid theme that matches the current website theme.
*/
function getMermaidTheme(): MermaidTheme {
if (typeof document === 'undefined') {
return 'light';
}

return document.documentElement.classList.contains('dark') ? 'dark' : 'light';
}

/**
* @description Initializes the Mermaid library for the selected theme.
*/
function initializeMermaid(theme: MermaidTheme) {
if (initializedMermaidTheme === theme) {
return;
}

initializedMermaidTheme = theme;
mermaid.initialize({
startOnLoad: false,
theme: 'base',
securityLevel: 'strict',
// Keep Mermaid styling fully controlled by MERMAID_THEME_VARIABLES.
themeCSS: '',
themeVariables: MERMAID_THEME_VARIABLES[theme]
});
}

let currentId = 0;

/**
* @description Generates a unique identifier.
* @returns {string} - A unique identifier.
*/
const uuid = (): string => `mermaid-${(currentId++).toString()}`;

interface MermaidDiagramProps {
graph: string;
}

/**
* @description This component renders Mermaid diagrams.
*
* @param {MermaidDiagramProps} props - The props for the MermaidDiagram component.
* @param {string} props.graph - The Mermaid graph to render.
*/
function MermaidDiagram({ graph }: Readonly<MermaidDiagramProps>) {
const [svg, setSvg] = useState<string | null>(null);
const [theme, setTheme] = useState<MermaidTheme>('light');

useEffect(() => {
setTheme(getMermaidTheme());

const observer = new MutationObserver(() => {
setTheme(getMermaidTheme());
});

observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });

return () => observer.disconnect();
}, []);

/**
* @description Renders the Mermaid diagram.
*/
useEffect(() => {
let mounted = true;

if (graph) {
try {
initializeMermaid(theme);
mermaid.mermaidAPI.render(uuid(), graph.trim(), (svgGraph) => {
if (mounted) {
setSvg(svgGraph);
}
});
} catch (e) {
if (mounted) {
setSvg(null);
}
// eslint-disable-next-line no-console
console.error(e);
}
} else {
setSvg(null);
}

return () => {
mounted = false;
};
}, [graph, theme]);

return <div dangerouslySetInnerHTML={{ __html: svg || '' }} />;
}
const MermaidDiagram = dynamic(() => import('./MermaidDiagram'), { ssr: false });

interface CodeComponentProps {
children: string;
Expand Down
133 changes: 133 additions & 0 deletions components/MDX/MermaidDiagram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { useEffect, useId, useState } from 'react';

type MermaidTheme = 'light' | 'dark';

const MERMAID_THEME_VARIABLES: Record<MermaidTheme, Record<string, string>> = {
light: {
primaryColor: '#EDFAFF',
primaryBorderColor: '#47BCEE',
secondaryColor: '#F4EFFC',
secondaryBorderColor: '#875AE2',
fontFamily: 'Inter, sans-serif',
fontSize: '18px',
primaryTextColor: '#242929',
tertiaryColor: '#F7F9FA',
tertiaryBorderColor: '#BFC6C7',
lineColor: '#BFC6C7',
mainBkg: '#EDFAFF',
secondBkg: '#F4EFFC',
tertiaryBkg: '#F7F9FA',
clusterBkg: '#F7F9FA',
clusterBorder: '#BFC6C7',
edgeLabelBackground: '#FFFFFF'
},
dark: {
primaryColor: '#1E293B',
primaryBorderColor: '#38BDF8',
secondaryColor: '#2E2459',
secondaryBorderColor: '#A87EFC',
fontFamily: 'Inter, sans-serif',
fontSize: '18px',
primaryTextColor: '#F8FAFC',
tertiaryColor: '#121825',
tertiaryBorderColor: '#475569',
lineColor: '#94A3B8',
mainBkg: '#1E293B',
secondBkg: '#2E2459',
tertiaryBkg: '#121825',
clusterBkg: '#121825',
clusterBorder: '#475569',
edgeLabelBackground: '#1E293B'
}
};

// Cache the theme Mermaid was initialized with across client-side page transitions.
let initializedMermaidTheme: MermaidTheme | null = null;

/**
* @description Returns the Mermaid theme that matches the current website theme.
*/
function getMermaidTheme(): MermaidTheme {
return document.documentElement.classList.contains('dark') ? 'dark' : 'light';
}

interface MermaidDiagramProps {
graph: string;
}

/**
* @description This component renders Mermaid diagrams.
*
* Mermaid is loaded via a dynamic import inside the render effect so it is
* never included in the initial JS bundle — it is fetched on demand only
* when a page actually contains a diagram.
*
* @param {MermaidDiagramProps} props - The props for the MermaidDiagram component.
* @param {string} props.graph - The Mermaid graph definition to render.
*/
export default function MermaidDiagram({ graph }: Readonly<MermaidDiagramProps>) {
const [svg, setSvg] = useState<string | null>(null);
const [theme, setTheme] = useState<MermaidTheme>('light');
const reactId = useId();
// Produce a stable, DOM-safe ID for the SVG element mermaid generates.
const diagramId = `mermaid${reactId.replace(/:/g, '')}`;

Check warning on line 73 in components/MDX/MermaidDiagram.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#replace()`.

See more on https://sonarcloud.io/project/issues?id=asyncapi_website&issues=AZ_CQwBDKVFiZXqfTkcC&open=AZ_CQwBDKVFiZXqfTkcC&pullRequest=5681

useEffect(() => {
setTheme(getMermaidTheme());

const observer = new MutationObserver(() => {
setTheme(getMermaidTheme());
});

observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });

return () => observer.disconnect();
}, []);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

useEffect(() => {
if (!graph) {
setSvg(null);

return;
}

let mounted = true;

async function render() {
try {
const { default: mermaid } = await import('mermaid');

if (initializedMermaidTheme !== theme) {
initializedMermaidTheme = theme;
mermaid.initialize({
startOnLoad: false,
theme: 'base',
securityLevel: 'strict',
// Keep Mermaid styling fully controlled by MERMAID_THEME_VARIABLES.
themeVariables: MERMAID_THEME_VARIABLES[theme]
});
}

const { svg: rendered } = await mermaid.render(diagramId, graph.trim());

if (mounted) {
setSvg(rendered);
}
} catch (e) {
if (mounted) {
setSvg(null);
}
// eslint-disable-next-line no-console
console.error(e);
}
}

render();

return () => {
mounted = false;
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}, [graph, theme, diagramId]);

return <div dangerouslySetInnerHTML={{ __html: svg ?? '' }} />;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
1 change: 1 addition & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const nextConfig = {
ignoreDuringBuilds: true
},
output: 'export',
serverExternalPackages: ['langium', '@mermaid-js/parser'],
webpack(config, { isServer }) {
if (!isServer) {
config.resolve.fallback.fs = false;
Expand Down
Loading
Loading