Commit d0c3d607 by archer

support mermaid

parent d057d20c
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mammoth": "^1.5.1", "mammoth": "^1.5.1",
"mermaid": "^8.13.5",
"mongoose": "^6.10.0", "mongoose": "^6.10.0",
"nanoid": "^4.0.1", "nanoid": "^4.0.1",
"next": "13.1.6", "next": "13.1.6",
...@@ -49,14 +50,14 @@ ...@@ -49,14 +50,14 @@
"react-markdown": "^8.0.5", "react-markdown": "^8.0.5",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^6.0.2", "rehype-katex": "^6.0.2",
"remark-breaks": "^3.0.3",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-math": "^5.1.1", "remark-math": "^5.1.1",
"request-ip": "^3.3.0", "request-ip": "^3.3.0",
"sass": "^1.58.3", "sass": "^1.58.3",
"tunnel": "^0.0.6", "tunnel": "^0.0.6",
"wxpay-v3": "^3.0.2", "wxpay-v3": "^3.0.2",
"zustand": "^4.3.5", "zustand": "^4.3.5"
"mermaid": "^8.13.5"
}, },
"devDependencies": { "devDependencies": {
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.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>
\ No newline at end of file
...@@ -29,7 +29,8 @@ const map = { ...@@ -29,7 +29,8 @@ const map = {
appStore: require('./icons/appStore.svg').default, appStore: require('./icons/appStore.svg').default,
menu: require('./icons/menu.svg').default, menu: require('./icons/menu.svg').default,
edit: require('./icons/edit.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; export type IconName = keyof typeof map;
......
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);
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 mermaid from 'mermaid';
import { Spinner } from '@chakra-ui/react'; import MyIcon from '../Icon';
interface MermaidCodeBlockProps { import styles from './index.module.scss';
code: string;
}
const MermaidCodeBlock: FC<MermaidCodeBlockProps> = ({ code }) => { const mermaidAPI = mermaid.mermaidAPI;
const [svg, setSvg] = useState<string | null>(null); mermaidAPI.initialize({
const [loading, setLoading] = useState<boolean>(true); startOnLoad: false,
const codeTimeoutIdRef = useRef<number | null>(null); 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(() => { useEffect(() => {
if (codeTimeoutIdRef.current) { try {
clearTimeout(codeTimeoutIdRef.current); 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]); }, [code]);
useEffect(() => { const onclickExport = useCallback(() => {
return () => { const svg = dom.current?.children[0];
if (codeTimeoutIdRef.current) { if (!svg) return;
clearTimeout(codeTimeoutIdRef.current);
} 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 ( return (
<> <Box position={'relative'}>
{loading ? ( <Box
<div className="loading"> ref={dom}
<img src="/imgs/loading.gif" alt="Loading..." /> className={styles.mermaid}
</div> minH={'50px'}
) : ( py={4}
<div dangerouslySetInnerHTML={{ __html: svg }}
className="mermaid-wrapper" />
dangerouslySetInnerHTML={svg ? { __html: svg } : undefined} <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);
import React from 'react'; 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-]': { 'code[class*=language-]': {
color: '#d4d4d4', color: '#d4d4d4',
textShadow: 'none', textShadow: 'none',
...@@ -277,3 +282,51 @@ export const codeLight: { [key: string]: React.CSSProperties } = { ...@@ -277,3 +282,51 @@ export const codeLight: { [key: string]: React.CSSProperties } = {
zIndex: '0' 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);
...@@ -416,3 +416,12 @@ ...@@ -416,3 +416,12 @@
} }
} }
} }
.mermaid {
overflow-x: auto;
svg {
height: auto !important;
width: auto;
}
}
import React, { memo, useMemo, useEffect } from 'react'; import React, { memo, useMemo, useEffect } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { formatLinkText } from '@/utils/tools';
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
import { useCopyData, formatLinkText } from '@/utils/tools';
import Icon from '@/components/Icon';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math'; import remarkMath from 'remark-math';
import remarkBreaks from 'remark-breaks';
import rehypeKatex from 'rehype-katex'; import rehypeKatex from 'rehype-katex';
import MermaidCodeBlock from './MermaidCodeBlock';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
import styles from './index.module.scss'; import styles from './index.module.scss';
import { codeLight } from './codeLight'; import CodeLight from './codeLight';
import Loading from './Loading';
import MermaidCodeBlock from './MermaidCodeBlock';
const Markdown = ({ const Markdown = ({
source, source,
...@@ -22,8 +21,6 @@ const Markdown = ({ ...@@ -22,8 +21,6 @@ const Markdown = ({
formatLink?: boolean; formatLink?: boolean;
isChatting?: boolean; isChatting?: boolean;
}) => { }) => {
const { copyData } = useCopyData();
const formatSource = useMemo(() => { const formatSource = useMemo(() => {
return formatLink ? formatLinkText(source) : source; return formatLink ? formatLinkText(source) : source;
}, [source, formatLink]); }, [source, formatLink]);
...@@ -31,53 +28,25 @@ const Markdown = ({ ...@@ -31,53 +28,25 @@ const Markdown = ({
return ( return (
<ReactMarkdown <ReactMarkdown
className={`markdown ${styles.markdown} className={`markdown ${styles.markdown}
${ ${isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''}
isChatting
? source === ""
? styles.waitingAnimation
: styles.animation
: ""
}
`} `}
remarkPlugins={[remarkMath]} remarkPlugins={[remarkGfm, remarkMath, remarkBreaks]}
rehypePlugins={[remarkGfm, rehypeKatex]} rehypePlugins={[rehypeKatex]}
components={{ components={{
pre: "div", pre: 'div',
code({ node, inline, className, children, ...props }) { code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className ||''); const match = /language-(\w+)/.exec(className || '');
const code = String(children);
if (match && match[1] === "mermaid") { if (match?.[1] === 'mermaid') {
return <MermaidCodeBlock code={code} />; return isChatting ? <Loading /> : <MermaidCodeBlock code={String(children)} />;
} }
return !inline && match ? ( return (
<Box my={3} borderRadius={"md"} overflow={"overlay"} backgroundColor={"#222"}> <CodeLight className={className} inline={inline} match={match} {...props}>
<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}>
{children} {children}
</code> </CodeLight>
); );
}, }
}} }}
linkTarget="_blank" linkTarget="_blank"
> >
......
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;
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment