support mermaid

This commit is contained in:
archer
2023-06-11 16:32:06 +08:00
parent d057d20c17
commit d0c3d60751
11 changed files with 914 additions and 119 deletions

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1686468581713" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2951" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M512 640.64a42.666667 42.666667 0 0 0 42.666667-42.666667v-341.333333h130.986666a21.333333 21.333333 0 0 0 14.250667-5.461333l2.688-2.901334a21.333333 21.333333 0 0 0-4.010667-29.909333l-165.717333-126.464a32 32 0 0 0-38.912 0.042667L329.472 218.453333a21.333333 21.333333 0 0 0 12.970667 38.229334H469.333333v341.333333a42.666667 42.666667 0 0 0 42.666667 42.666667z m229.674667-298.368a42.666667 42.666667 0 0 0 4.992 85.034667H853.333333v426.666666H170.666667v-426.666666h106.666666a42.666667 42.666667 0 0 0 0-85.333334H170.666667a85.333333 85.333333 0 0 0-85.333334 85.333334v426.666666a85.333333 85.333333 0 0 0 85.333334 85.333334h682.666666a85.333333 85.333333 0 0 0 85.333334-85.333334v-426.666666a85.333333 85.333333 0 0 0-85.333334-85.333334h-106.666666z" fill="#000000" p-id="2952"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -29,7 +29,8 @@ const map = {
appStore: require('./icons/appStore.svg').default,
menu: require('./icons/menu.svg').default,
edit: require('./icons/edit.svg').default,
inform: require('./icons/inform.svg').default
inform: require('./icons/inform.svg').default,
export: require('./icons/export.svg').default
};
export type IconName = keyof typeof map;

View File

@@ -0,0 +1,17 @@
import React, { memo } from 'react';
import { Box } from '@chakra-ui/react';
const Loading = () => {
return (
<Box
w={'100%'}
h={'80px'}
backgroundImage={'url("/imgs/loading.gif")'}
backgroundSize={'contain'}
backgroundRepeat={'no-repeat'}
backgroundPosition={'center'}
/>
);
};
export default memo(Loading);

View File

@@ -1,63 +1,99 @@
import React, { FC, useEffect, useState, useRef } from 'react';
import React, { useEffect, useRef, memo, useCallback, useState } from 'react';
import { Box } from '@chakra-ui/react';
// @ts-ignore
import mermaid from 'mermaid';
import { Spinner } from '@chakra-ui/react';
import MyIcon from '../Icon';
interface MermaidCodeBlockProps {
code: string;
}
import styles from './index.module.scss';
const MermaidCodeBlock: FC<MermaidCodeBlockProps> = ({ code }) => {
const [svg, setSvg] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const codeTimeoutIdRef = useRef<number | null>(null);
const mermaidAPI = mermaid.mermaidAPI;
mermaidAPI.initialize({
startOnLoad: false,
theme: 'base',
themeVariables: {
fontSize: '14px',
primaryColor: '#d6e8ff',
primaryTextColor: '#485058',
primaryBorderColor: '#fff',
lineColor: '#5A646E',
secondaryColor: '#B5E9E5',
tertiaryColor: '#485058'
}
});
const MermaidBlock = ({ code }: { code: string }) => {
const dom = useRef<HTMLDivElement>(null);
const [svg, setSvg] = useState('');
useEffect(() => {
if (codeTimeoutIdRef.current) {
clearTimeout(codeTimeoutIdRef.current);
try {
mermaidAPI.render('mermaid-svg', code, (svgCode: string) => {
setSvg(svgCode);
});
} catch (error) {
console.log(error);
}
codeTimeoutIdRef.current = window.setTimeout(() => {
setLoading(true);
const mermaidAPI = (mermaid as any).mermaidAPI as any;
mermaidAPI.initialize({ startOnLoad: false, theme: 'forest' });
try {
mermaidAPI.parse(code);
mermaidAPI.render('mermaid-svg', code, (svgCode: string) => {
setSvg(svgCode);
setLoading(false);
});
} catch (error) {
console.error('Error parsing Mermaid code:', '\n', error, '\n', 'Code:', code);
setLoading(false);
return;
}
}, 1000);
}, [code]);
useEffect(() => {
return () => {
if (codeTimeoutIdRef.current) {
clearTimeout(codeTimeoutIdRef.current);
}
const onclickExport = useCallback(() => {
const svg = dom.current?.children[0];
if (!svg) return;
const w = svg.clientWidth * 4;
const h = svg.clientHeight * 4;
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// 绘制白色背景
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, w, h);
const img = new Image();
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(dom.current.innerHTML)}`;
img.onload = () => {
ctx.drawImage(img, 0, 0, w, h);
const jpgDataUrl = canvas.toDataURL('image/jpeg', 1);
const a = document.createElement('a');
a.href = jpgDataUrl;
a.download = 'mermaid.jpg';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
img.onerror = (e) => {
console.log(e);
};
}, []);
return (
<>
{loading ? (
<div className="loading">
<img src="/imgs/loading.gif" alt="Loading..." />
</div>
) : (
<div
className="mermaid-wrapper"
dangerouslySetInnerHTML={svg ? { __html: svg } : undefined}
/>
)}
</>
<Box position={'relative'}>
<Box
ref={dom}
className={styles.mermaid}
minH={'50px'}
py={4}
dangerouslySetInnerHTML={{ __html: svg }}
/>
<MyIcon
name={'export'}
w={'20px'}
position={'absolute'}
color={'myGray.600'}
_hover={{
color: 'myBlue.700'
}}
right={0}
top={0}
cursor={'pointer'}
onClick={onclickExport}
/>
</Box>
);
};
export default MermaidCodeBlock;
export default memo(MermaidBlock);

View File

@@ -1,5 +1,10 @@
import React from 'react';
export const codeLight: { [key: string]: React.CSSProperties } = {
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
import Icon from '@/components/Icon';
import { useCopyData } from '@/utils/tools';
const codeLight: { [key: string]: React.CSSProperties } = {
'code[class*=language-]': {
color: '#d4d4d4',
textShadow: 'none',
@@ -277,3 +282,51 @@ export const codeLight: { [key: string]: React.CSSProperties } = {
zIndex: '0'
}
};
const CodeLight = ({
children,
className,
inline,
match,
...props
}: {
children: React.ReactNode & React.ReactNode[];
className?: string;
inline?: boolean;
match: RegExpExecArray | null;
}) => {
const { copyData } = useCopyData();
if (!inline && match) {
return (
<Box my={3} borderRadius={'md'} overflow={'overlay'} backgroundColor={'#222'}>
<Flex
className="code-header"
py={2}
px={5}
backgroundColor={useColorModeValue('#323641', 'gray.600')}
color={'#fff'}
fontSize={'sm'}
userSelect={'none'}
>
<Box flex={1}>{match?.[1]}</Box>
<Flex cursor={'pointer'} onClick={() => copyData(String(children))} alignItems={'center'}>
<Icon name={'copy'} width={15} height={15} fill={'#fff'}></Icon>
<Box ml={1}></Box>
</Flex>
</Flex>
<SyntaxHighlighter style={codeLight as any} language={match?.[1]} PreTag="pre" {...props}>
{String(children)}
</SyntaxHighlighter>
</Box>
);
}
return (
<code className={className} {...props}>
{children}
</code>
);
};
export default React.memo(CodeLight);

View File

@@ -416,3 +416,12 @@
}
}
}
.mermaid {
overflow-x: auto;
svg {
height: auto !important;
width: auto;
}
}

View File

@@ -1,17 +1,16 @@
import React, { memo, useMemo, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
import { useCopyData, formatLinkText } from '@/utils/tools';
import Icon from '@/components/Icon';
import { formatLinkText } from '@/utils/tools';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import remarkBreaks from 'remark-breaks';
import rehypeKatex from 'rehype-katex';
import MermaidCodeBlock from './MermaidCodeBlock';
import 'katex/dist/katex.min.css';
import styles from './index.module.scss';
import { codeLight } from './codeLight';
import CodeLight from './codeLight';
import Loading from './Loading';
import MermaidCodeBlock from './MermaidCodeBlock';
const Markdown = ({
source,
@@ -22,8 +21,6 @@ const Markdown = ({
formatLink?: boolean;
isChatting?: boolean;
}) => {
const { copyData } = useCopyData();
const formatSource = useMemo(() => {
return formatLink ? formatLinkText(source) : source;
}, [source, formatLink]);
@@ -31,53 +28,25 @@ const Markdown = ({
return (
<ReactMarkdown
className={`markdown ${styles.markdown}
${
isChatting
? source === ""
? styles.waitingAnimation
: styles.animation
: ""
}
${isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''}
`}
remarkPlugins={[remarkMath]}
rehypePlugins={[remarkGfm, rehypeKatex]}
remarkPlugins={[remarkGfm, remarkMath, remarkBreaks]}
rehypePlugins={[rehypeKatex]}
components={{
pre: "div",
pre: 'div',
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className ||'');
const code = String(children);
const match = /language-(\w+)/.exec(className || '');
if (match && match[1] === "mermaid") {
return <MermaidCodeBlock code={code} />;
if (match?.[1] === 'mermaid') {
return isChatting ? <Loading /> : <MermaidCodeBlock code={String(children)} />;
}
return !inline && match ? (
<Box my={3} borderRadius={"md"} overflow={"overlay"} backgroundColor={"#222"}>
<Flex
className="code-header"
py={2}
px={5}
backgroundColor={useColorModeValue("#323641", "gray.600")}
color={"#fff"}
fontSize={"sm"}
userSelect={"none"}
>
<Box flex={1}>{match?.[1]}</Box>
<Flex cursor={"pointer"} onClick={() => copyData(code)} alignItems={"center"}>
<Icon name={"copy"} width={15} height={15} fill={"#fff"}></Icon>
<Box ml={1}></Box>
</Flex>
</Flex>
<SyntaxHighlighter style={codeLight as any} language={match?.[1]} PreTag="pre" {...props}>
{code}
</SyntaxHighlighter>
</Box>
) : (
<code className={className} {...props}>
return (
<CodeLight className={className} inline={inline} match={match} {...props}>
{children}
</code>
</CodeLight>
);
},
}
}}
linkTarget="_blank"
>

View File

@@ -1,19 +0,0 @@
declare module "mermaid" {
import mermaidAPI from "mermaid";
const mermaid: any;
export default mermaid;
// 扩展 mermaidAPI
interface MermaidAPI extends mermaidAPI.mermaidAPI {
contentLoaded: (
targetEl: Element,
options?: mermaidAPI.mermaidAPI.Config
) => void;
}
const mermaidAPIInstance: MermaidAPI;
export default mermaidAPIInstance;
}
type Dispatch = (action: Action) => void;