import { memo, useCallback, useMemo } from 'react';
import MarkdownX from './core';
import type { MarkdownProps } from './types';
import useTyping from './core/hooks/useTyping';
import { useProviderContext } from '@agentscope-ai/chat';
import classNames from 'classnames';
import Null from './core/components/Null';
import CodeBlock from './core/components/CodeBlock';
import DisabledImage from './core/components/DisableImage';
import Media from './core/components/Media';
import Raw from './core/components/Raw';
import { ErrorBoundary } from "react-error-boundary";
import useCitationsData from './core/hooks/useCitationsData';
import Latex from '@ant-design/x-markdown/plugins/Latex';
import { citationsExtension } from './core/plugins/citations';
import { CursorComponent, cursorExtension } from './core/plugins/cursor';
import markedFootnote from 'marked-footnote'
import Link from './core/components/Link';
// 缓存不变的 dompurify 配置
const EMPTY_DOMPURIFY_CONFIG = {
ALLOWED_TAGS: [],
};
/**
* 检测浏览器是否支持正则表达式的 lookbehind assertions
* iOS Safari < 16.4 不支持此特性
*/
function supportsLookbehindAssertions(): boolean {
try {
// 尝试创建包含正向后行断言的正则表达式
new RegExp('(?<=a)b');
return true;
} catch (e) {
return false;
}
}
const isSupportsLookbehindAssertions = supportsLookbehindAssertions();
export default memo(function (props: MarkdownProps) {
const baseFontSize = props.baseFontSize || 14;
const baseLineHeight = props.baseLineHeight || 1.7;
const content = useTyping({ content: props.content, typing: props.typing && !props.animation });
const prefixCls = useProviderContext().getPrefixCls('markdown');
const {
raw = false,
allowHtml = false,
} = props;
const {
citationsData,
citationsDataCount,
CitationComponent
} = useCitationsData({ citations: props.citations, citationsMap: props.citationsMap });
const components = useMemo(() => ({
code: CodeBlock,
style: Null,
script: Null,
img: props.disableImage ? DisabledImage : Media,
citation: CitationComponent,
'custom-cursor': CursorComponent,
a: Link,
...props.components,
}), [props.disableImage, CitationComponent, props.components]);
const dompurifyConfig = useMemo(() => ({
ADD_TAGS: ['custom-cursor', 'citation']
}), []);
// 使用 useMemo 缓存 extensions 配置
const { extensions, walkTokens } = useMemo(() => {
const exts = Latex()
exts.push(cursorExtension());
if (citationsDataCount > 0) exts.push(citationsExtension(citationsData));
const f = markedFootnote({
sectionClass: `${prefixCls}-footnotes`
});
exts.push(...f.extensions);
return { extensions: exts, walkTokens: f.walkTokens };
}, [citationsDataCount, citationsData]);
// // 使用 useMemo 缓存 config 对象
const config = useMemo(() => ({
extensions,
walkTokens,
// 当 allowHtml 为 false 时,仅放行安全的内联 HTML 标签,其余转义
...(!allowHtml && {
renderer: {
html(token: { text?: string; raw?: string }) {
const text = token.text || token.raw || '';
if (/^
$/i.test(text.trim())) return '
';
return text.replace(//g, '>');
}
}
})
}), [extensions, allowHtml]);
const resolvedContent = content || '';
const fallback = ;
const fallbackRender = useCallback((...args: unknown[]) => {
console.error(args);
return ;
}, [resolvedContent, baseFontSize, baseLineHeight]);
const markdownStyle = useMemo(() => ({ fontSize: baseFontSize, lineHeight: baseLineHeight }), [baseFontSize, baseLineHeight]);
if (raw || !isSupportsLookbehindAssertions) return fallback;
return
});