Skip to content

Fix MermaidWithFallback diagram display #2609

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 24, 2025
Merged
Changes from all commits
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
203 changes: 162 additions & 41 deletions docusaurus/src/components/MermaidWithFallback.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// src/components/DocusaurusMermaidFileFallback.js
import React, { useEffect, useState, useRef } from 'react';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { useColorMode } from '@docusaurus/theme-common';
import Icon from './Icon'

Expand All @@ -13,9 +12,11 @@ export default function DocusaurusMermaidFileFallback({
const [chartContent, setChartContent] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [renderFailed, setRenderFailed] = useState(false);
const [shouldRender, setShouldRender] = useState(false);
const { colorMode } = useColorMode();
const imgRef = useRef(null);
const mermaidContainerRef = useRef(null);
const hasRendered = useRef(false);

// Determine which image to display based on the current theme
const imageToShow = colorMode === 'dark' && fallbackImageDark
Expand All @@ -37,10 +38,31 @@ export default function DocusaurusMermaidFileFallback({
);
};

// Clean and prepare mermaid content
const cleanMermaidContent = useCallback(() => {
if (mermaidContainerRef.current) {
const mermaidDiv = mermaidContainerRef.current.querySelector('.mermaid');
if (mermaidDiv) {
// Remove any existing SVG and reset
mermaidDiv.innerHTML = chartContent;
mermaidDiv.removeAttribute('data-processed');
// Remove any mermaid-generated attributes
const attributes = [...mermaidDiv.attributes];
attributes.forEach(attr => {
if (attr.name.startsWith('data-mermaid')) {
mermaidDiv.removeAttribute(attr.name);
}
});
}
}
}, [chartContent]);

// Load chart content from file
useEffect(() => {
if (chartFile) {
setIsLoading(true);
setRenderFailed(false);
hasRendered.current = false;

fetch(chartFile)
.then(response => {
Expand All @@ -53,6 +75,8 @@ export default function DocusaurusMermaidFileFallback({
console.log('Mermaid file loaded successfully:', chartFile);
setChartContent(content.trim());
setIsLoading(false);
// Trigger initial render attempt
setShouldRender(true);
})
.catch(error => {
console.error('Error loading Mermaid chart file:', error);
Expand All @@ -61,58 +85,155 @@ export default function DocusaurusMermaidFileFallback({
});
}
}, [chartFile]);

// Force Mermaid to use the current theme when it renders
useEffect(() => {
// When content is loaded or theme changes, we'll need to re-render Mermaid
if (!isLoading && chartContent && mermaidContainerRef.current) {
const renderMermaidWithTheme = async () => {
try {
// Dynamically import mermaid to ensure it's loaded in the browser
const mermaid = (await import('mermaid')).default;

// Initialize mermaid with the current theme
mermaid.initialize({
startOnLoad: false,
theme: colorMode === 'dark' ? 'dark' : 'default',
securityLevel: 'loose',
});

// Clear previous content
const mermaidDiv = mermaidContainerRef.current.querySelector('.mermaid');
if (mermaidDiv) {
mermaidDiv.innerHTML = chartContent;

// Run mermaid to render the diagram
await mermaid.run({
nodes: [mermaidDiv],
});
}
} catch (error) {
console.error('Mermaid rendering failed:', error);
setRenderFailed(true);
}
};

// Check if component is in active tab
const checkIfVisible = useCallback(() => {
if (!mermaidContainerRef.current) return false;

// Check if the container is actually visible (not in hidden tab)
const rect = mermaidContainerRef.current.getBoundingClientRect();
const isInViewport = rect.width > 0 && rect.height > 0;

// Also check if parent tab is active
const tabPanel = mermaidContainerRef.current.closest('[role="tabpanel"]');
const isTabActive = tabPanel ? !tabPanel.hasAttribute('hidden') : true;

return isInViewport && isTabActive;
}, []);

// Render Mermaid diagram
const renderMermaidDiagram = useCallback(async () => {
if (!chartContent || !mermaidContainerRef.current) return;

// Don't render if not visible or already rendered
if (!checkIfVisible()) {
return;
}

try {
console.log('Attempting to render Mermaid diagram...');

renderMermaidWithTheme();
// Clean previous content
cleanMermaidContent();

// Dynamically import mermaid
const mermaid = (await import('mermaid')).default;

// Also set a timeout to check if rendering was successful
// Initialize mermaid with current theme
mermaid.initialize({
startOnLoad: false,
theme: colorMode === 'dark' ? 'dark' : 'default',
securityLevel: 'loose',
});

const mermaidDiv = mermaidContainerRef.current.querySelector('.mermaid');
if (mermaidDiv && chartContent) {
// Set content
mermaidDiv.innerHTML = chartContent;

// Generate unique ID
const uniqueId = `mermaid-${chartFile.replace(/[^a-zA-Z0-9]/g, '')}-${Date.now()}`;
mermaidDiv.id = uniqueId;

// Render the diagram
await mermaid.run({
nodes: [mermaidDiv],
});

hasRendered.current = true;
console.log('Mermaid diagram rendered successfully');
}
} catch (error) {
console.error('Mermaid rendering failed:', error);
setRenderFailed(true);
}
}, [chartContent, colorMode, cleanMermaidContent, checkIfVisible, chartFile]);

// Listen for tab changes and visibility
useEffect(() => {
if (!shouldRender || isLoading || renderFailed) return;

// Initial render attempt
const initialRender = () => {
if (checkIfVisible()) {
renderMermaidDiagram();
}
};

// Small delay for initial render
const initialTimer = setTimeout(initialRender, 100);

// Listen for tab changes
const handleTabChange = () => {
setTimeout(() => {
if (checkIfVisible() && !hasRendered.current) {
hasRendered.current = false; // Reset for re-render
renderMermaidDiagram();
}
}, 50);
};

// Listen for clicks on tabs (Docusaurus tab behavior)
const tabButtons = document.querySelectorAll('[role="tab"]');
tabButtons.forEach(button => {
button.addEventListener('click', handleTabChange);
});

// Also listen for visibility changes
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'hidden') {
handleTabChange();
}
});
});

const tabPanel = mermaidContainerRef.current?.closest('[role="tabpanel"]');
if (tabPanel) {
observer.observe(tabPanel, { attributes: true });
}

return () => {
clearTimeout(initialTimer);
tabButtons.forEach(button => {
button.removeEventListener('click', handleTabChange);
});
observer.disconnect();
};
}, [shouldRender, isLoading, renderFailed, renderMermaidDiagram, checkIfVisible]);

// Re-render when theme changes
useEffect(() => {
if (!isLoading && chartContent && shouldRender && !renderFailed) {
hasRendered.current = false;
setTimeout(() => {
if (checkIfVisible()) {
renderMermaidDiagram();
}
}, 100);
}
}, [colorMode, isLoading, chartContent, shouldRender, renderFailed, checkIfVisible, renderMermaidDiagram]);

// Check for rendering success/failure
useEffect(() => {
if (!isLoading && chartContent && shouldRender && !renderFailed) {
const timer = setTimeout(() => {
if (mermaidContainerRef.current) {
if (mermaidContainerRef.current && checkIfVisible()) {
const mermaidDiv = mermaidContainerRef.current.querySelector('.mermaid');
if (
if (mermaidDiv && (
!mermaidDiv.querySelector('svg') ||
mermaidDiv.textContent.includes('Syntax error') ||
mermaidDiv.querySelector('.error-icon')
) {
)) {
console.warn('Mermaid rendering appears to have failed, switching to fallback');
setRenderFailed(true);
}
}
}, 2000);
}, 3000);

return () => clearTimeout(timer);
}
}, [chartContent, isLoading, colorMode]);
}, [isLoading, chartContent, shouldRender, renderFailed, checkIfVisible]);

// Initialize zoom on the fallback image
useEffect(() => {
Expand Down