chore: render katex in markdown
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import React, { useMemo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import styled from 'styled-components';
|
||||
import HighlightCode from '../highlight-code';
|
||||
import './index.less';
|
||||
import { escapeBrackets, escapeDollarNumber, escapeMhchem } from './utils';
|
||||
|
||||
interface FullMarkdownProps {
|
||||
content: string;
|
||||
theme?: 'light' | 'dark';
|
||||
}
|
||||
|
||||
const Wrapper = styled.div.attrs(() => ({
|
||||
className: 'markdown-viewer'
|
||||
}))`
|
||||
line-height: 2;
|
||||
`;
|
||||
|
||||
const CodeViewer = (props: any) => {
|
||||
const { children, className, node, theme, ...rest } = props;
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return match ? (
|
||||
<HighlightCode code={children} lang={match[1]} theme={theme} />
|
||||
) : (
|
||||
<code {...rest} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
};
|
||||
|
||||
const FullMarkdown: React.FC<FullMarkdownProps> = ({
|
||||
content,
|
||||
theme = 'light'
|
||||
}) => {
|
||||
const escapedContent = useMemo(() => {
|
||||
return escapeMhchem(escapeBrackets(escapeDollarNumber(content)));
|
||||
}, [content]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[[remarkMath], remarkGfm]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
components={{
|
||||
code(props) {
|
||||
return <CodeViewer {...props} theme={theme}></CodeViewer>;
|
||||
},
|
||||
a: ({ href, children, ...props }) => (
|
||||
<a
|
||||
href={sanitizeUrl(href)}
|
||||
{...props}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
>
|
||||
{escapedContent}
|
||||
</ReactMarkdown>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default FullMarkdown;
|
||||
@@ -22,6 +22,7 @@
|
||||
font-weight: 700;
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-small);
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.hj-wrapper {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
import { Checkbox, Image, Typography } from 'antd';
|
||||
import { unescape } from 'lodash';
|
||||
import { TokensList, marked } from 'marked';
|
||||
@@ -52,7 +53,8 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
|
||||
}, []);
|
||||
|
||||
const generateImgSrc = useCallback(
|
||||
(src: string | null) => {
|
||||
(url: string | null) => {
|
||||
const src = sanitizeUrl(url || '');
|
||||
if (!src) {
|
||||
return '';
|
||||
}
|
||||
@@ -186,6 +188,11 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
|
||||
return;
|
||||
}
|
||||
const imgs = document.querySelectorAll('.markdown-viewer img');
|
||||
const links = document.querySelectorAll('.markdown-viewer a');
|
||||
links.forEach((link) => {
|
||||
// set target blank for all links
|
||||
link.setAttribute('target', '_blank');
|
||||
});
|
||||
imgs.forEach((img) => {
|
||||
const src = img.getAttribute('src');
|
||||
img.setAttribute('src', generateImgSrc(src));
|
||||
|
||||
@@ -14,3 +14,42 @@ export const unescape = (str = '') => {
|
||||
? str.replace(reEscapedHtml, (entity) => htmlUnescapes[entity] || "'")
|
||||
: str;
|
||||
};
|
||||
|
||||
export function escapeDollarNumber(text: string) {
|
||||
let escapedText = '';
|
||||
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
let char = text[i];
|
||||
const nextChar = text[i + 1] || ' ';
|
||||
|
||||
if (char === '$' && nextChar >= '0' && nextChar <= '9') {
|
||||
char = '\\$';
|
||||
}
|
||||
|
||||
escapedText += char;
|
||||
}
|
||||
|
||||
return escapedText;
|
||||
}
|
||||
|
||||
export function escapeBrackets(text: string) {
|
||||
const pattern =
|
||||
/(```[\S\s]*?```|`.*?`)|\\\[([\S\s]*?[^\\])\\]|\\\((.*?)\\\)/g;
|
||||
return text.replaceAll(
|
||||
pattern,
|
||||
(match, codeBlock, squareBracket, roundBracket) => {
|
||||
if (codeBlock) {
|
||||
return codeBlock;
|
||||
} else if (squareBracket) {
|
||||
return `$$${squareBracket}$$`;
|
||||
} else if (roundBracket) {
|
||||
return `$${roundBracket}$`;
|
||||
}
|
||||
return match;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function escapeMhchem(text: string) {
|
||||
return text.replaceAll('$\\ce{', '$\\\\ce{').replaceAll('$\\pu{', '$\\\\pu{');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user