chore: render katex in markdown

This commit is contained in:
jialin
2025-02-18 19:51:22 +08:00
parent ea34431cf7
commit 4c77151877
26 changed files with 693 additions and 208 deletions
@@ -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 {
+8 -1
View File
@@ -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));
+39
View File
@@ -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{');
}