MermaidViewer.tsx 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. import React, { type JSX, useEffect, useRef } from 'react';
  2. import { GROWI_IS_CONTENT_RENDERING_ATTR } from '@growi/core/dist/consts';
  3. import mermaid from 'mermaid';
  4. import { v7 as uuidV7 } from 'uuid';
  5. import { useNextThemes } from '~/stores-universal/use-next-themes';
  6. import loggerFactory from '~/utils/logger';
  7. const logger = loggerFactory('growi:features:mermaid:MermaidViewer');
  8. type MermaidViewerProps = {
  9. value: string;
  10. };
  11. export const MermaidViewer = React.memo(
  12. (props: MermaidViewerProps): JSX.Element => {
  13. const { value } = props;
  14. const { isDarkMode } = useNextThemes();
  15. const ref = useRef<HTMLDivElement>(null);
  16. useEffect(() => {
  17. let rafId: number | undefined;
  18. (async () => {
  19. if (ref.current != null && value != null) {
  20. mermaid.initialize({
  21. theme: isDarkMode ? 'dark' : undefined,
  22. });
  23. try {
  24. // Attempting to render multiple Mermaid diagrams using `mermaid.run` can cause duplicate SVG IDs.
  25. // This is because it uses `Date.now()` for ID generation.
  26. // ID generation logic: https://github.com/mermaid-js/mermaid/blob/5b241bbb97f81d37df8a84da523dfa53ac13bfd1/packages/mermaid/src/utils.ts#L755-L764
  27. // Related issue: https://github.com/mermaid-js/mermaid/issues/4650
  28. // Instead of `mermaid.run`, we use `mermaid.render` which allows us to assign a unique ID.
  29. const id = `mermaid-${uuidV7()}`;
  30. const { svg } = await mermaid.render(id, value, ref.current);
  31. ref.current.innerHTML = svg;
  32. // Delay the "done" signal to the next animation frame so the browser has a chance
  33. // to compute the SVG layout before the auto-scroll system re-scrolls.
  34. rafId = requestAnimationFrame(() => {
  35. ref.current?.setAttribute(
  36. GROWI_IS_CONTENT_RENDERING_ATTR,
  37. 'false',
  38. );
  39. });
  40. } catch (err) {
  41. logger.error(err);
  42. ref.current?.setAttribute(GROWI_IS_CONTENT_RENDERING_ATTR, 'false');
  43. }
  44. }
  45. })();
  46. return () => {
  47. if (rafId != null) {
  48. cancelAnimationFrame(rafId);
  49. }
  50. };
  51. }, [isDarkMode, value]);
  52. return value ? (
  53. <div
  54. ref={ref}
  55. key={value}
  56. {...{ [GROWI_IS_CONTENT_RENDERING_ATTR]: 'true' }}
  57. >
  58. {value}
  59. </div>
  60. ) : (
  61. <div key={value}></div>
  62. );
  63. },
  64. );
  65. MermaidViewer.displayName = 'MermaidViewer';