Skip to content
Closed
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
182 changes: 18 additions & 164 deletions components/MDX/MDX.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
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';

Check warning on line 4 in components/MDX/MDX.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import of 'useState'.

See more on https://sonarcloud.io/project/issues?id=asyncapi_website&issues=AZ-_vuA__sm3M2mr0vcg&open=AZ-_vuA__sm3M2mr0vcg&pullRequest=5679

Check warning on line 4 in components/MDX/MDX.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import of 'useEffect'.

See more on https://sonarcloud.io/project/issues?id=asyncapi_website&issues=AZ-_vuA__sm3M2mr0vcf&open=AZ-_vuA__sm3M2mr0vcf&pullRequest=5679
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
import {
TwitterDMButton,
TwitterFollowButton,
TwitterHashtagButton,
TwitterMentionButton,
TwitterMomentShare,
TwitterOnAirButton,
TwitterShareButton,
TwitterTimelineEmbed,
TwitterTweetEmbed,
TwitterVideoEmbed
} from 'react-twitter-embed';
import YouTube from 'react-youtube-embed';

// Lazy-loaded heavy dependencies via next/dynamic (ssr: false).
// MermaidDiagram: ~1.5MB (isolated in ../MermaidDiagram.tsx)
// react-twitter-embed + react-youtube-embed: ~300KB each
// Only downloaded when a page actually uses diagrams, Twitter embeds,
// or YouTube videos — reducing initial JS bundle by ~1.8MB on typical MDX pages.
// Fixes #5667, #3186.

const MermaidDiagram = dynamic(() => import('./MermaidDiagram'), { ssr: false });

const TwitterTweetEmbed = dynamic(
() => import('react-twitter-embed').then((mod) => ({ default: mod.TwitterTweetEmbed })),
{ ssr: false }
);

const YouTube = dynamic(() => import('react-youtube-embed'), { ssr: false });

import Asyncapi3ChannelComparison from '../Asyncapi3Comparison/Asyncapi3ChannelComparison';
import Asyncapi3IdAndAddressComparison from '../Asyncapi3Comparison/Asyncapi3IdAndAddressComparison';
Expand All @@ -41,146 +44,6 @@
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 || '' }} />;
}

interface CodeComponentProps {
children: string;
Expand Down Expand Up @@ -213,7 +76,7 @@
const language = maybeLanguage && maybeLanguage.length >= 2 ? maybeLanguage[1] : undefined;

if (language === 'mermaid') {
return <MermaidDiagram graph={children} />;
return React.createElement(MermaidDiagram as any, { graph: children });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return (
Expand Down Expand Up @@ -471,16 +334,7 @@
DocsCards,
GeneratorInstallation,
NewsletterSubscribe,
TwitterTimelineEmbed,
TwitterShareButton,
TwitterFollowButton,
TwitterHashtagButton,
TwitterMentionButton,
TwitterTweetEmbed,
TwitterMomentShare,
TwitterDMButton,
TwitterVideoEmbed,
TwitterOnAirButton,
Profiles,
Visualizer
});
Expand Down
117 changes: 117 additions & 0 deletions components/MDX/MermaidDiagram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
'use client';

import { useEffect, useRef } from 'react';
import mermaidAPI from 'mermaid';
import type { CSSProperties } from 'react';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

interface MermaidDiagramProps {
chart: string;
id?: string;
style?: CSSProperties;
className?: string;
}

// Security: sanitize diagram content for safe rendering
const sanitizeDiagram = (code: string): string => {
// Strip <script> tags and event handlers to prevent XSS
return code
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/\bon\w+\s*=/gi, 'data-blocked=');
};

const getMermaidThemeVariables = (isDark: boolean) => ({
darkMode: isDark,
theme: isDark ? 'dark' : 'default',
themeVariables: isDark
? {
primaryColor: '#4a9eff',
primaryTextColor: '#f0f0f0',
primaryBorderColor: '#555',
lineColor: '#aaa',
secondaryColor: '#2d2d2d',
tertiaryColor: '#1a1a1a',
}
: {
primaryColor: '#2b6cb0',
primaryTextColor: '#333',
primaryBorderColor: '#ccc',
lineColor: '#666',
secondaryColor: '#f5f5f5',
tertiaryColor: '#e8e8e8',
},
});

export default function MermaidDiagram({
chart,
id = 'mermaid-diagram',
style,
className,
}: MermaidDiagramProps) {

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=asyncapi_website&issues=AZ-_vuFl_sm3M2mr0vch&open=AZ-_vuFl_sm3M2mr0vch&pullRequest=5679
const containerRef = useRef<HTMLDivElement>(null);
const renderedRef = useRef(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Give each diagram a unique id.

id defaults to the constant 'mermaid-diagram'. If a page contains two or more mermaid blocks, the rendered <div> elements share one DOM id, and mermaidAPI.render receives the same mermaid-diagram-svg id for each instance. Mermaid removes the existing element with that id before rendering, so the diagrams can overwrite each other.

Derive the default from useId.

♻️ Proposed fix
-import { useEffect, useRef } from 'react';
+import { useEffect, useId, useRef } from 'react';
@@
-export default function MermaidDiagram({
-  chart,
-  id = 'mermaid-diagram',
-  style,
-  className,
-}: MermaidDiagramProps) {
+export default function MermaidDiagram({ graph, id, style, className }: Readonly<MermaidDiagramProps>) {
+  const reactId = useId();
+  const diagramId = id ?? `mermaid-${reactId.replace(/:/g, '')}`;
   const containerRef = useRef<HTMLDivElement>(null);

Then use diagramId in mermaidAPI.render, the effect dependencies, and the container id.

🧰 Tools
🪛 ESLint

[error] 44-49: Replace ⏎··chart,⏎··id·=·'mermaid-diagram',⏎··style,⏎··className,⏎ with ·chart,·id·=·'mermaid-diagram',·style,·className·

(prettier/prettier)

🪛 GitHub Check: SonarCloud Code Analysis

[warning] 44-49: Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=asyncapi_website&issues=AZ-_vuFl_sm3M2mr0vch&open=AZ-_vuFl_sm3M2mr0vch&pullRequest=5679

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/MDX/MermaidDiagram.tsx` around lines 44 - 51, Update
MermaidDiagram to derive its fallback identifier from React’s useId instead of
the shared 'mermaid-diagram' constant, while preserving an explicitly provided
id. Store the resolved value as diagramId and use it consistently for
mermaidAPI.render, the rendering effect dependencies, and the container’s DOM
id.


useEffect(() => {
if (!containerRef.current || renderedRef.current) return;

const isDark =
typeof window !== 'undefined' &&
document.documentElement.getAttribute('data-theme') === 'dark';

Check failure on line 58 in components/MDX/MermaidDiagram.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `.dataset` over `getAttribute(…)`.

See more on https://sonarcloud.io/project/issues?id=asyncapi_website&issues=AZ-_vuFl_sm3M2mr0vci&open=AZ-_vuFl_sm3M2mr0vci&pullRequest=5679
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const renderDiagram = async () => {
try {
const sanitized = sanitizeDiagram(chart);
const svg = await mermaidAPI.render(`${id}-svg`, sanitized);
if (containerRef.current) {
containerRef.current.innerHTML = svg;
renderedRef.current = true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check the declared and locked mermaid version, and existing render call sites.
fd -H -t f 'package.json' -d 2 --exec grep -n '"mermaid"' {} \;
fd -H -t f 'package-lock.json' -d 1 --exec grep -n -A3 '"node_modules/mermaid"' {} \;
rg -nP '\bmermaid(API)?\.(render|initialize)\s*\(' --type=ts --type=tsx --type=js

Repository: asyncapi/website

Length of output: 487


🌐 Web query:

mermaid 9.3.0 mermaid.render return value promise or string

💡 Result:

In Mermaid version 9.3.0, the mermaid.render function returns a Promise [1][2]. This Promise resolves to a RenderResult object [1][2][3], which contains the following properties: - svg (string): The generated SVG code [1][4]. - bindFunctions (function, optional): A function that, when called with an element as an argument, binds necessary event listeners (like tooltips or clicks) to the rendered diagram [3][5][4]. - diagramType (string): The type of diagram that was rendered (e.g., 'flowchart', 'sequence') [1][4]. Usage example: const { svg, bindFunctions, diagramType } = await mermaid.render('myId', 'graph TD; A-->B'); document.getElementById('output').innerHTML = svg; bindFunctions?.(document.getElementById('output')); Note that mermaid.render in version 9.3.0 handles the queueing of rendering tasks, ensuring that multiple calls are executed serially to prevent race conditions, unlike the internal mermaidAPI.render method which executes immediately [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "repo files count:"
git ls-files | wc -l

echo
echo "find mermaid-related package files:"
git ls-files | rg '(^|/)(package\.json|package-lock\.json|yarn.lock|pnpm-lock.yaml)$' || true

echo
echo "mermaid mentions in package manifests/locks:"
for f in $(git ls-files | rg '(^|/)(package\.json|package-lock\.json|yarn.lock|pnpm-lock.yaml)$'); do
  printf '\n--- %s ---\n' "$f"
  rg -n '"mermaid"|mermaid' "$f" | head -80 || true
done

echo
echo "MermaidDiagram outline and relevant source:"
if git ls-files | rg -q 'components/MDX/MermaidDiagram.tsx'; then
  wc -l components/MDX/MermaidDiagram.tsx
  cat -n components/MDX/MermaidDiagram.tsx | sed -n '1,180p'
else
  fd -i 'MermaidDiagram' .
fi

echo
echo "render call sites in tracked TS/TSX/JS files:"
git ls-files | rg '\.(ts|tsx|js|jsx)$' | xargs -r rg -n '\bmermaid(API)?\.(render|initialize)\s*\(' || true

Repository: asyncapi/website

Length of output: 1084


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "package manager indicators:"
git ls-files | rg '(^|/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb|bun\.lock)$' || true

for name in npm_package_lockfile npm yarn pnpm bun; do
  [ -f package.json ] || continue
  python3 - <<'PY'
import json
from pathlib import Path

p=json.loads(Path("package.json").read_text())
print("name:", p.get("name"))
print("engines:", p.get("engines"))
print("volta:", p.get("volta"))
print("packageManager:", p.get("packageManager"))
print("workspaces:", p.get("workspaces"))
PY
done

echo
echo "Mermaid API import/export shape in lockfile:"
sed -n '20740,20755p' package-lock.json

echo
echo "Import statement:"
sed -n '1,20p' components/MDX/MermaidDiagram.tsx

Repository: asyncapi/website

Length of output: 1823


🌐 Web query:

Mermaid 9.3.0 source mermaidAPI.render TypeScript

💡 Result:

In Mermaid version 9.3.0, the mermaidAPI.render function is available but is marked as deprecated for external use in favor of the high-level mermaid.render function [1][2]. TypeScript Usage and Characteristics: 1. Deprecation Status: The mermaidAPI.render function includes a @deprecated JSDoc tag explicitly recommending the use of mermaid.render instead [1]. 2. Function Signature (v9.3.0): The mermaidAPI.render function is traditionally signatured to accept a callback [3][4]: render(id: string, text: string, cb?: (svgCode: string, bindFunctions?: (element: Element) => void) => void, svgContainingElement?: Element) => string; 3. Behavior: Unlike the high-level mermaid.render function, which wraps rendering in an asynchronous queue to prevent race conditions during concurrent calls, mermaidAPI.render executes immediately [5][2]. Recommendation: For integration, you should prefer the top-level mermaid.render function. If you must use mermaidAPI.render, be aware that it lacks automatic queuing and error handling, making it susceptible to race conditions if called multiple times concurrently [5][2]. Note that mermaidAPI is intended for internal library use and its interface is subject to change or removal in future major versions [1][5][2].

Citations:


🌐 Web query:

site:github.com mermaid-js/mermaid v9.3.0 mermaidAPI render

💡 Result:

In Mermaid v9.3.0, the mermaidAPI.render function is available but is marked as deprecated for external use [1][2][3]. The official recommendation is to use the mermaid.render function instead [1][2][4][3]. The mermaidAPI.render function was historically used to render diagram text into an SVG element [5][6]. However, it is explicitly flagged as deprecated in the source code of v9.3.0 with the following guidance: "use the mermaid.render function instead of mermaid.mermaidAPI.render" [1][2][3]. Key considerations for v9.3.0: 1. Recommended Practice: Developers are encouraged to use the top-level mermaid.render function. This is because the top-level function provides safer handling, such as wrapping rendering calls in an asynchronous queue to prevent conflicts, whereas direct calls to mermaidAPI.render bypass these safeguards [4]. 2. Deprecation Status: While mermaidAPI.render exists in v9.3.0, it is considered internal or legacy, and reliance on it is discouraged in favor of the standard API [1][2]. 3. Usage Pattern (Legacy): Historically, mermaidAPI.render took parameters including an ID, the graph definition text, and a callback function (or an optional SVG containing element) [5][6]. Modern usage with the mermaid.render function is preferred for better compatibility and stability [4].

Citations:


Use the non-deprecated Mermaid render API.

The repository pins Mermaid 9.3.0, where mermaidAPI.render runs immediately, is deprecated, and does not automatically queue rendering calls. Switch this to the supported mermaid.render(id, sanitized) call shape so multiple diagrams do not interfere with each other.

🧰 Tools
🪛 ESLint

[error] 63-63: Expected blank line after variable declarations.

(newline-after-var)


[error] 64-67: Expected blank line before this statement.

(padding-line-between-statements)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/MDX/MermaidDiagram.tsx` around lines 60 - 67, Update renderDiagram
to use Mermaid’s supported mermaid.render(id, sanitized) API instead of the
deprecated mermaidAPI.render call, preserving the existing sanitized input, SVG
assignment, and renderedRef behavior.

Source: Linters/SAST tools

} catch (error) {
if (containerRef.current) {
containerRef.current.innerHTML = `<pre style="color:red;padding:1rem;border:1px solid red;border-radius:4px;">Mermaid render error: ${error instanceof Error ? error.message : String(error)}</pre>`;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not interpolate the error message into innerHTML.

Mermaid parse errors include fragments of the diagram source. That source comes from MDX content. Interpolating it into innerHTML allows markup injection in the error path, which bypasses the securityLevel: 'strict' protection that applies only to the success path.

Build the error node with DOM APIs and set the text with textContent.

🔒️ Proposed fix
       } catch (error) {
         if (containerRef.current) {
-          containerRef.current.innerHTML = `<pre style="color:red;padding:1rem;border:1px solid red;border-radius:4px;">Mermaid render error: ${error instanceof Error ? error.message : String(error)}</pre>`;
+          const pre = document.createElement('pre');
+
+          pre.className = 'text-red-600 p-4 border border-red-600 rounded';
+          pre.textContent = `Mermaid render error: ${error instanceof Error ? error.message : String(error)}`;
+          containerRef.current.replaceChildren(pre);
         }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
if (containerRef.current) {
containerRef.current.innerHTML = `<pre style="color:red;padding:1rem;border:1px solid red;border-radius:4px;">Mermaid render error: ${error instanceof Error ? error.message : String(error)}</pre>`;
}
}
} catch (error) {
if (containerRef.current) {
const pre = document.createElement('pre');
pre.className = 'text-red-600 p-4 border border-red-600 rounded';
pre.textContent = `Mermaid render error: ${error instanceof Error ? error.message : String(error)}`;
containerRef.current.replaceChildren(pre);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/MDX/MermaidDiagram.tsx` around lines 68 - 72, Update the catch
block in MermaidDiagram to avoid interpolating the error message into
containerRef.current.innerHTML. Create the error element using DOM APIs, set its
styling and message through safe properties including textContent, and append or
replace it in the container while preserving the existing error text and
presentation.

};

// Initialize mermaid once
const initAndRender = async () => {
mermaidAPI.initialize({
startOnLoad: false,
securityLevel: 'strict',
...getMermaidThemeVariables(isDark),
});
await renderDiagram();
};

initAndRender();

// Observe theme changes for light/dark switching
const observer = new MutationObserver(() => {
const newIsDark =
document.documentElement.getAttribute('data-theme') === 'dark';

Check failure on line 90 in components/MDX/MermaidDiagram.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `.dataset` over `getAttribute(…)`.

See more on https://sonarcloud.io/project/issues?id=asyncapi_website&issues=AZ-_vuFl_sm3M2mr0vcj&open=AZ-_vuFl_sm3M2mr0vcj&pullRequest=5679
mermaidAPI.initialize({
startOnLoad: false,
securityLevel: 'strict',
...getMermaidThemeVariables(newIsDark),
});
renderedRef.current = false;
renderDiagram();
});

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

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

return (
<div
ref={containerRef}
id={id}
style={style}
className={className}
aria-label="Mermaid diagram"
/>
);
}
Loading