Skip to content

Commit

Permalink
mermaid diagram copy as png to clipboard
Browse files Browse the repository at this point in the history
  • Loading branch information
embernet committed May 20, 2024
1 parent 7fbff0a commit f2b2631
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 12 deletions.
54 changes: 51 additions & 3 deletions web_ui/src/MermaidDiagram.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import React, { useEffect, useRef, useState } from "react";
import { Box, Collapse, Button, Typography } from "@mui/material";
import React, { useEffect, useRef, useState, useContext } from "react";
import { Box, Collapse, Button, Typography, IconButton, Toolbar, Tooltip } from "@mui/material";
import mermaid from "mermaid";
import { v4 as uuidv4 } from 'uuid';
import { memo } from 'react';

const MermaidDiagram = memo(({ markdown }) => {
import { SidekickClipboardContext } from './SidekickClipboardContext';
import CollectionsOutlinedIcon from '@mui/icons-material/CollectionsOutlined';

const MermaidDiagram = memo(({ markdown, escapedMarkdown }) => {
const mermaidRef = useRef(null);
const mermaidId = `mermaid-diagram-${uuidv4()}`;
const [svg, setSvg] = useState(null);
const [error, setError] = useState(null);
const [showError, setShowError] = useState(false);
const sidekickClipboard = useContext(SidekickClipboardContext);

useEffect(() => {
mermaid.initialize({
Expand All @@ -26,6 +30,39 @@ const MermaidDiagram = memo(({ markdown }) => {
}
}, [svg]);

const handleCopyImage = (event) => {
event.stopPropagation();
if (mermaidRef.current) {
const svgElement = mermaidRef.current.querySelector('svg');
const scaleFactor = 2;
const rect = svgElement.getBoundingClientRect();
const width = rect.width;
const height = rect.height;

const svgElementClone = svgElement.cloneNode(true);
svgElementClone.setAttribute('width', width * scaleFactor);
svgElementClone.setAttribute('height', height * scaleFactor);
const svgData = new XMLSerializer().serializeToString(svgElementClone);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const image = new Image();
image.onload = () => {
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
canvas.toBlob(blob => {
(async () => {
await sidekickClipboard.write({
png: blob,
sidekickObject: { markdown: escapedMarkdown }
});
})();
});
};
image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData);
}
};

useEffect(() => {
let isCancelled = false;

Expand Down Expand Up @@ -61,12 +98,23 @@ const MermaidDiagram = memo(({ markdown }) => {
{
!error
?
<Box>
<Toolbar>
<Tooltip title="Copy image as PNG to clipboard for external use (markdown will be placed in the internal clipboard)" arrow>
<IconButton edge="start" color="inherit"
aria-label="copy image to clipboard"
onClick={handleCopyImage}>
<CollectionsOutlinedIcon/>
</IconButton>
</Tooltip>
</Toolbar>
<div
className="mermaid-diagram"
id={'id-'+mermaidId} key={'key-'+mermaidId} ref={mermaidRef}
style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}
>
</div>
</Box>
:
<Box>
<Button onClick={() => setShowError(!showError)} aria-expanded={showError} aria-label="show more" style={{ color: 'red' }}>
Expand Down
4 changes: 3 additions & 1 deletion web_ui/src/SidekickClipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ export default class SidekickClipboard {
return new Promise((resolve, reject) => {
try {
let item;
if (sidekickClipboardItems?.html) {
if (sidekickClipboardItems?.png) {
item = new ClipboardItem({ 'image/png': sidekickClipboardItems.png });
} else if (sidekickClipboardItems?.html) {
const htmlBlob = new Blob([sidekickClipboardItems.html], { type: 'text/html' });
item = new ClipboardItem({ 'text/html': htmlBlob });
} else if (sidekickClipboardItems?.text) {
Expand Down
21 changes: 13 additions & 8 deletions web_ui/src/SidekickMarkdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import ReactMarkdown from 'react-markdown';
import { ClassNames } from "@emotion/react";
import { useContext, useState, useEffect } from 'react';
import { Card, Toolbar, Typography, Box, IconButton } from '@mui/material';
import { Card, Toolbar, Typography, Box, IconButton, Tooltip } from '@mui/material';

// Icons
import ContentCopyIcon from '@mui/icons-material/ContentCopy';

import { SystemContext } from './SystemContext';
Expand Down Expand Up @@ -52,16 +54,19 @@ const SidekickMarkdown = memo(({ markdown }) => {
<Toolbar className={ClassNames.toolbar}>
<Typography sx={{ mr: 2 }}>{language}</Typography>
<Box sx={{ display: "flex", width: "100%", flexDirection: "row", ml: "auto" }}>
<IconButton edge="start" color="inherit" aria-label="copy to clipboard"
onClick={(event) => { (async () => {
await sidekickClipboard.write({text: code, sidekickObject: { markdown: codeMarkdown }});
})(); event.stopPropagation(); }}>
<ContentCopyIcon/>
</IconButton>
<Tooltip title="Copy markdown to clipboard" arrow>
<IconButton edge="start" color="inherit" aria-label="copy to clipboard"
onClick={(event) => { (async () => {
await sidekickClipboard.write({text: code, sidekickObject: { markdown: codeMarkdown }});
})(); event.stopPropagation(); }}>
<ContentCopyIcon/>
</IconButton>
</Tooltip>
</Box>

</Toolbar>
{(language === "mermaid") ?
<MermaidDiagram markdown={code}/>
<MermaidDiagram markdown={code} escapedMarkdown={codeMarkdown}/>
:
<SyntaxHighlighter sx={{ width: "100%" }} lineProps={{style: {wordBreak: 'break-all', whiteSpace: 'pre-wrap'}}} language={language} wrapLines={true} style={docco}>
{code}
Expand Down

0 comments on commit f2b2631

Please sign in to comment.