Browse Source

impl Drawio component

Yuki Takei 3 years ago
parent
commit
62249f4a1a

+ 5 - 0
packages/remark-drawio-plugin/src/components/Drawio.module.scss

@@ -0,0 +1,5 @@
+.mxgraph-container {
+  margin: 20px 0;
+  border: 1px solid transparent;
+  border-radius: 4px;
+}

+ 34 - 2
packages/remark-drawio-plugin/src/components/Drawio.tsx

@@ -1,13 +1,45 @@
 import React, { ReactNode } from 'react';
 
+import { generateMxgraphData } from '../utils/embed';
+
+import styles from './Drawio.module.scss';
+
+
 type Props = {
+  diagramIndex: number,
+  bol?: number,
+  eol?: number,
   drawioEmbedUri?: string,
   children?: ReactNode,
 }
 
 export const Drawio = (props: Props): JSX.Element => {
-  const { children } = props;
+  const {
+    diagramIndex, bol, eol, children,
+  } = props;
   const drawioEmbedUri = props.drawioEmbedUri ?? 'https://embed.diagrams.net/';
 
-  return <span>{children}</span>;
+  if (children == null) {
+    return <></>;
+  }
+
+  const code = children instanceof Array
+    ? children.map(e => e?.toString()).join('')
+    : children.toString();
+
+  const mxgraphData = generateMxgraphData(code, diagramIndex);
+
+  const mxgraphHtml = `<div class="mxgraph" style="max-width: 100%; border: 1px solid transparent" data-mxgraph="${mxgraphData}"></div>`;
+
+  return (
+    <div
+      key={`drawio-viewer-${diagramIndex}`}
+      className={`mxgraph-container ${styles['mxgraph-container']}`}
+      data-begin-line-number-of-markdown={bol}
+      data-end-line-number-of-markdown={eol}
+    >
+      {/* eslint-disable-next-line react/no-danger */}
+      <div dangerouslySetInnerHTML={{ __html: mxgraphHtml }} />
+    </div>
+  );
 };

+ 1 - 0
packages/remark-drawio-plugin/src/index.ts

@@ -1,2 +1,3 @@
 export * from './components/Drawio';
 export * from './services/renderer/remark-drawio-plugin';
+export * from './utils/embed';

+ 10 - 5
packages/remark-drawio-plugin/src/services/renderer/remark-drawio-plugin.ts

@@ -3,7 +3,7 @@ import { Plugin } from 'unified';
 import { Node } from 'unist';
 import { visit } from 'unist-util-visit';
 
-const SUPPORTED_ATTRIBUTES = ['drawioEmbedUri'];
+const SUPPORTED_ATTRIBUTES = ['diagramIndex', 'drawioEmbedUri', 'bol', 'eol'];
 
 type Lang = 'drawio';
 
@@ -15,21 +15,26 @@ export type DrawioRemarkPluginParams = {
   drawioEmbedUri?: string,
 }
 
-function rewriteNode(node: Node, options: DrawioRemarkPluginParams) {
+function rewriteNode(node: Node, index: number, options: DrawioRemarkPluginParams) {
   const data = node.data ?? (node.data = {});
 
   node.type = 'paragraph';
   node.children = [{ type: 'text', value: node.value }];
   data.hName = 'drawio';
-  data.hProperties = { drawioEmbedUri: options.drawioEmbedUri };
+  data.hProperties = {
+    diagramIndex: index,
+    drawioEmbedUri: options.drawioEmbedUri,
+    bol: node.position?.start.line,
+    eol: node.position?.end.line,
+  };
 }
 
 export const remarkPlugin: Plugin<[DrawioRemarkPluginParams]> = function(options = {}) {
   return (tree) => {
-    visit(tree, (node) => {
+    visit(tree, (node, index) => {
       if (node.type === 'code') {
         if (isDrawioBlock(node.lang)) {
-          rewriteNode(node, options);
+          rewriteNode(node, index ?? 0, options);
         }
       }
     });

+ 110 - 0
packages/remark-drawio-plugin/src/utils/embed.ts

@@ -0,0 +1,110 @@
+// transplanted from https://github.com/jgraph/drawio-tools/blob/d46977060ffad70cae5a9059a2cbfcd8bcf420de/tools/convert.html
+import pako from 'pako';
+import xmldoc from 'xmldoc';
+
+const validateInputData = (input: string): boolean => {
+  let data = input;
+
+  try {
+    const doc = new xmldoc.XmlDocument(data);
+    const diagram = doc.valueWithPath('diagram');
+    data = diagram;
+  }
+  catch (e) {
+    // ignore
+  }
+
+  try {
+    data = Buffer.from(data, 'base64').toString('binary');
+  }
+  catch (e) {
+    throw new Error(`Base64 to binary failed: ${e}`);
+  }
+
+  if (data.length > 0) {
+    try {
+      data = pako.inflateRaw(Uint8Array.from(data, c => c.charCodeAt(0)), { to: 'string' });
+    }
+    catch (e) {
+      throw new Error(`inflateRaw failed: ${e}`);
+    }
+  }
+
+  try {
+    data = decodeURIComponent(data);
+  }
+  catch (e) {
+    throw new Error(`decodeURIComponent failed: ${e}`);
+  }
+
+  return true;
+};
+
+const escapeHTML = (string): string => {
+  if (typeof string !== 'string') {
+    return string;
+  }
+  return string.replace(/[&'`"<>]/g, (match): string => {
+    return {
+      '&': '&amp;',
+      "'": '&#x27;',
+      '`': '&#x60;',
+      '"': '&quot;',
+      '<': '&lt;',
+      '>': '&gt;',
+    }[match] ?? match;
+  });
+};
+
+export const generateMxgraphData = (code: string, idx: number): string => {
+  const trimedCode = code.trim();
+  if (!trimedCode) {
+    return '';
+  }
+
+  try {
+    validateInputData(trimedCode);
+  }
+  catch (e) {
+    return `
+  <div class="drawio-viewer-index-${idx} markdownItDrawioViewer markdownItDrawioViewerError">
+    <p>MarkdownItDrawioViewer Error: ${e}</p>
+  </div>
+  `;
+  }
+
+  let xml;
+  try {
+    // may be XML Format <mxfile><diagram> ... </diagram></mxfile>
+    const doc = new xmldoc.XmlDocument(trimedCode);
+    const diagram = doc.valueWithPath('diagram');
+    if (diagram) {
+      xml = trimedCode;
+    }
+  }
+  catch (e) {
+    // may be NOT XML Format
+    xml = `
+<mxfile version="6.8.9" editor="www.draw.io" type="atlas">
+  <mxAtlasLibraries/>
+  <diagram>${trimedCode}</diagram>
+</mxfile>
+`;
+  }
+
+  const mxGraphData = {
+    editable: false,
+    highlight: '#0000ff',
+    nav: false,
+    toolbar: null,
+    edit: null,
+    resize: true,
+    lightbox: 'false',
+    // "check-visible-state": false,
+    // "auto-fit": false,
+    // move: false,
+    xml,
+  };
+
+  return escapeHTML(JSON.stringify(mxGraphData));
+};