69 lines
1.9 KiB
TypeScript
69 lines
1.9 KiB
TypeScript
import React from 'react';
|
|
import ReactMarkdown from 'react-markdown';
|
|
import remarkGfm from 'remark-gfm';
|
|
import './MarkdownContent.css';
|
|
|
|
type Props = {
|
|
content?: string;
|
|
referenceMap?: Record<string, string>;
|
|
className?: string;
|
|
};
|
|
|
|
function rehypeHighlightRefs(referenceMap?: Record<string, string>) {
|
|
return function (tree: any) {
|
|
function walk(node: any) {
|
|
if (!node.children) return;
|
|
const next = [];
|
|
for (const child of node.children) {
|
|
if (child.type === 'text') {
|
|
const parts = child.value.split(/(\[\d+\])/g);
|
|
if (parts.length > 1) {
|
|
for (const part of parts) {
|
|
if (!part) continue;
|
|
const m = part.match(/^\[(\d+)\]$/);
|
|
if (m) {
|
|
const num = m[1];
|
|
const itemId = referenceMap?.[num];
|
|
next.push({
|
|
type: 'element',
|
|
tagName: 'span',
|
|
properties: {
|
|
className: 'ref-cite',
|
|
'data-tooltip': itemId ? `ID: ${itemId}` : `引用 ${num}`,
|
|
},
|
|
children: [{ type: 'text', value: part }],
|
|
});
|
|
} else {
|
|
next.push({ type: 'text', value: part });
|
|
}
|
|
}
|
|
} else {
|
|
next.push(child);
|
|
}
|
|
} else {
|
|
walk(child);
|
|
next.push(child);
|
|
}
|
|
}
|
|
node.children = next;
|
|
}
|
|
walk(tree);
|
|
};
|
|
}
|
|
|
|
export default function MarkdownContent({ content, referenceMap, className = '' }: Props): JSX.Element | null {
|
|
if (!content) return null;
|
|
const rehypePlugins = [() => rehypeHighlightRefs(referenceMap)];
|
|
|
|
return (
|
|
<div className={`markdown-body ${className}`}>
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
rehypePlugins={rehypePlugins}
|
|
>
|
|
{content}
|
|
</ReactMarkdown>
|
|
</div>
|
|
);
|
|
}
|