Преглед изворни кода

Implementation of renderMermaidDiagram

Shun Miyazawa пре 8 месеци
родитељ
комит
e8574e70e4
1 измењених фајлова са 75 додато и 7 уклоњено
  1. 75 7
      apps/app/src/features/mermaid/components/MermaidViewer.tsx

+ 75 - 7
apps/app/src/features/mermaid/components/MermaidViewer.tsx

@@ -1,13 +1,73 @@
 import React, { useRef, useEffect, type JSX } from 'react';
 import React, { useRef, useEffect, type JSX } from 'react';
 
 
-import mermaid from 'mermaid';
+import mermaid, { type DetailedError } from 'mermaid';
+import dedent from 'ts-dedent';
+import { v7 as uuidV7 } from 'uuid';
 
 
 import { useNextThemes } from '~/stores-universal/use-next-themes';
 import { useNextThemes } from '~/stores-universal/use-next-themes';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:features:mermaid:MermaidViewer');
 
 
 type MermaidViewerProps = {
 type MermaidViewerProps = {
   value: string
   value: string
 }
 }
 
 
+let decoder: HTMLDivElement;
+
+/**
+ * Decodes HTML, source: {@link https://github.com/shrpne/entity-decode/blob/v2.0.1/browser.js}
+ *
+ * @param html - HTML as a string
+ * @returns Unescaped HTML
+ */
+const entityDecode = (html: string): string => {
+  decoder = decoder || document.createElement('div');
+  // Escape HTML before decoding for HTML Entities
+  html = escape(html).replace(/%26/g, '&').replace(/%23/g, '#').replace(/%3B/g, ';');
+  decoder.innerHTML = html;
+
+  return unescape(decoder.textContent!);
+};
+
+const renderMermaidDiagram = async(node: HTMLElement) => {
+  const errors: DetailedError[] = [];
+  let txt: string;
+
+  /*! Check if previously processed */
+  if (node.getAttribute('data-processed') != null) {
+    return;
+  }
+
+  node.setAttribute('data-processed', 'true');
+
+  // Fetch the graph definition including tags
+  txt = node.innerHTML;
+
+  // transforms the html to pure text
+  txt = dedent(entityDecode(txt)) // removes indentation, required for YAML parsing
+    .trim()
+    .replace(/<br\s*\/?>/gi, '<br/>');
+
+
+  try {
+    const id = `mermaid-${uuidV7()}`;
+    const { svg, bindFunctions } = await mermaid.render(id, txt, node);
+    node.innerHTML = svg;
+    if (bindFunctions != null) {
+      bindFunctions(node);
+    }
+  }
+  catch (error) {
+    logger.error('Error rendering diagram', error);
+  }
+
+  if (errors.length > 0) {
+    // TODO: We should be throwing an error object.
+    throw errors[0];
+  }
+};
+
 export const MermaidViewer = React.memo((props: MermaidViewerProps): JSX.Element => {
 export const MermaidViewer = React.memo((props: MermaidViewerProps): JSX.Element => {
   const { value } = props;
   const { value } = props;
 
 
@@ -16,12 +76,20 @@ export const MermaidViewer = React.memo((props: MermaidViewerProps): JSX.Element
   const ref = useRef<HTMLDivElement>(null);
   const ref = useRef<HTMLDivElement>(null);
 
 
   useEffect(() => {
   useEffect(() => {
-    if (ref.current != null && value != null) {
-      mermaid.initialize({
-        theme: isDarkMode ? 'dark' : undefined,
-      });
-      mermaid.run({ nodes: [ref.current] });
-    }
+    (async() => {
+      if (ref.current != null && value != null) {
+        mermaid.initialize({
+          theme: isDarkMode ? 'dark' : undefined,
+        });
+        try {
+          await renderMermaidDiagram(ref.current);
+        }
+        catch (err) {
+          logger.error(err);
+        }
+      }
+    })();
+
   }, [isDarkMode, value]);
   }, [isDarkMode, value]);
 
 
   return (
   return (