= React.memo((props) => {
return parser.parse(displayContent);
}, [displayContent, parser]);
- if (!displayContent) return null;
+ // Memoize footer rendering to prevent unnecessary re-renders
+ const footerElement = useMemo(() => {
+ if (!footer) return null;
+ return (
+
+ {footer}
+
+ );
+ }, [footer, prefixCls, footerClassName, mergedFooterStyle]);
+
+ if (!displayContent && !footer) return null;
return (
- {renderer.render(htmlString)}
+
+ {displayContent ? renderer.render(htmlString) : null}
+
+ {footerElement}
);
});
diff --git a/packages/x-markdown/src/XMarkdown/interface.ts b/packages/x-markdown/src/XMarkdown/interface.ts
index dc73e88a6..09c71f81b 100644
--- a/packages/x-markdown/src/XMarkdown/interface.ts
+++ b/packages/x-markdown/src/XMarkdown/interface.ts
@@ -142,6 +142,21 @@ interface XMarkdownProps {
* @description DOMPurify configuration for HTML sanitization and XSS protection
*/
dompurifyConfig?: DOMPurifyConfig;
+ /**
+ * @description 显示在文档末尾的 React 节点
+ * @description React node displayed at the end of document
+ */
+ footer?: React.ReactNode;
+ /**
+ * @description 文档末尾的 React 节点的 CSS 类名
+ * @description CSS class name for the footer React node
+ */
+ footerClassName?: string;
+ /**
+ * @description 文档末尾的 React 节点的内联样式
+ * @description Inline styles for the footer React node
+ */
+ footerStyle?: React.CSSProperties;
}
export type { XMarkdownProps, Token, Tokens, StreamStatus, ComponentProps, StreamingOption };
diff --git a/packages/x-markdown/src/plugins/Latex/__test__/__snapshots__/index.test.tsx.snap b/packages/x-markdown/src/plugins/Latex/__test__/__snapshots__/index.test.tsx.snap
index cadeda2cf..15ac1b92d 100644
--- a/packages/x-markdown/src/plugins/Latex/__test__/__snapshots__/index.test.tsx.snap
+++ b/packages/x-markdown/src/plugins/Latex/__test__/__snapshots__/index.test.tsx.snap
@@ -6,95 +6,99 @@ exports[`LaTeX Plugin should handle LaTeX with surrounding text 1`] = `
class="ant-x-markdown x-markdown"
style="direction: ltr;"
>
-
- This is an equation:
-
+
+
+ This is an equation:
-
-
- E
-
-
-
- =
-
-
-
-
-
- m
+
+
+ E
+
+
+
+ =
+
+
+
- c
+ m
+ c
+
+
-
+
- 2
+
+ 2
+
@@ -107,9 +111,9 @@ exports[`LaTeX Plugin should handle LaTeX with surrounding text 1`] = `
-
- in text
-
+ in text
+
+
`;
@@ -120,217 +124,221 @@ exports[`LaTeX Plugin should handle align* syntax replacement 1`] = `
class="ant-x-markdown x-markdown"
style="direction: ltr;"
>
-
-
+
+
-
+
-
+
- x
+
+ x
+
+
+
+
-
-
-
-
-
-
+
+
+
-
-
-
-
-
- =
-
-
- y
-
-
-
-
- y
+
+
+
+ =
+
+
+
+ y
+
+
+
+
+ y
+
+
+
+
-
-
-
-
-
-
+
+
+
-
-
-
+
-
- =
-
-
- z
+
+ =
+
+
+
+ z
+
+
+
+
-
-
-
-
-
-
+
+
+
@@ -341,8 +349,8 @@ exports[`LaTeX Plugin should handle align* syntax replacement 1`] = `
-
-
+
+
`;
@@ -353,165 +361,107 @@ exports[`LaTeX Plugin should handle complex LaTeX expressions 1`] = `
class="ant-x-markdown x-markdown"
style="direction: ltr;"
>
-
-
+
+
-
+
-
+
- i
-
-
- =
-
-
- 1
+
+ i
+
+
+ =
+
+
+ 1
+
-
-
-
-
-
- ∑
-
-
-
-
-
+ class="pstrut"
+ style="height: 3.05em;"
+ />
+
- n
+ ∑
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- x
-
-
-
-
-
- i
+
+ n
+
@@ -527,150 +477,98 @@ exports[`LaTeX Plugin should handle complex LaTeX expressions 1`] = `
>
-
-
-
- =
-
-
-
-
-
-
+
+ x
+
+
-
- j
-
-
- =
-
+ class="pstrut"
+ style="height: 2.7em;"
+ />
- 1
+
+ i
+
-
-
-
-
- ∏
-
+ class="vlist-s"
+ >
+
-
-
-
- m
-
-
+
-
-
-
-
-
-
-
-
+
+
+ =
+
+
-
- y
-
+ class="strut"
+ style="height: 3.0652em; vertical-align: -1.4138em;"
+ />
+
+
+
+
+ j
+
+
+ =
+
+
+ 1
+
+
+
+
+
+
+
+
+ ∏
+
+
+
+
- j
+
+ m
+
@@ -712,105 +660,73 @@ exports[`LaTeX Plugin should handle complex LaTeX expressions 1`] = `
>
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`LaTeX Plugin should handle content without LaTeX 1`] = `
-
-`;
-
-exports[`LaTeX Plugin should handle empty content 1`] = ``;
-
-exports[`LaTeX Plugin should handle mixed LaTeX syntaxes 1`] = `
-
-
-
- Inline:
-
-
-
-
-
-
-
- x
-
+ class="mspace"
+ style="margin-right: 0.1667em;"
+ />
+ y
+
+
-
+
- 2
+
+ j
+
+
+
+
+
+
+
+
+
@@ -820,860 +736,984 @@ exports[`LaTeX Plugin should handle mixed LaTeX syntaxes 1`] = `
-
- and block:
-
+
+
+
+
+`;
+
+exports[`LaTeX Plugin should handle content without LaTeX 1`] = `
+
+`;
+
+exports[`LaTeX Plugin should handle empty content 1`] = ``;
+
+exports[`LaTeX Plugin should handle mixed LaTeX syntaxes 1`] = `
+
+
+
+
+ Inline:
-
- ∫
-
+ class="strut"
+ style="height: 0.8641em;"
+ />
+ x
+
+
-
- 0
-
-
-
-
-
-
+ class="pstrut"
+ style="height: 2.7em;"
+ />
- 1
+
+ 2
+
-
-
-
-
-
-
-
-
-
-
- f
-
-
- (
-
-
- x
-
-
- )
-
-
- d
-
-
- x
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`LaTeX Plugin should handle multiple LaTeX formulas 1`] = `
-
-
-
-
-
-
-
-
-
-
- a
-
-
-
- +
-
-
-
-
-
-
- b
-
-
- and
-
+ and block:
-
+ ∫
+
+
-
+
- d
+
+ 0
+
-
-
-
-
-
-
-
+
- c
+
+ 1
+
+
+
+
-
-
-
-
-
-
+
+
+
+ class="mspace"
+ style="margin-right: 0.1667em;"
+ />
+
+ f
+
+
+ (
+
+
+ x
+
+
+ )
+
+
+ d
+
+
+ x
+
-
-
+
+
`;
-exports[`LaTeX Plugin should not throw error by default 1`] = `
+exports[`LaTeX Plugin should handle multiple LaTeX formulas 1`] = `
-
- latex:
-
-
-
+
+
- \\begin{align
+
+
+
+
+
+
+ a
+
+
+
+ +
+
+
+
+
+
+
+ b
+
+
+
+
+
-
-
-
-
-`;
-
-exports[`LaTeX Plugin should render block LaTeX with $$..$$ syntax 1`] = `
-
-
-
-
+ and
-
+
-
+
- b
+
+ d
+
-
-
-
-
-
-
+ style="top: -3.23em;"
+ >
+
+
+
+
- a
+
+ c
+
+
+
+
-
-
-
-
-
-
+
+
+
+
-
-
-
+
+
`;
-exports[`LaTeX Plugin should render block LaTeX with \\[..\\] syntax 1`] = `
+exports[`LaTeX Plugin should not throw error by default 1`] = `
-
-
+
+ latex:
+
+
+ \\begin{align
+
+
+
+
+
+
+`;
+
+exports[`LaTeX Plugin should render block LaTeX with $$..$$ syntax 1`] = `
+
+
+
+
+
+
-
-
+
+
-
+
+
+
+
+ b
+
+
+
+
+
+
+
+
+
+
+
+ a
+
+
+
+
+
- b
+
-
-
-
-
-
-
-
- a
+
-
-
-
-
-
-
-
+ class="mclose nulldelimiter"
+ />
-
-
-
+
+
`;
-exports[`LaTeX Plugin should render inline LaTeX with $$\\n..\\n$$ syntax 1`] = `
+exports[`LaTeX Plugin should render block LaTeX with \\[..\\] syntax 1`] = `
-
- latex:
+
-
-
- f
-
-
- (
-
-
- λ
-
-
- x
-
-
-
- +
-
-
-
-
-
-
- (
-
-
- 1
-
-
-
- −
-
-
-
-
-
-
- λ
-
-
- )
-
-
- y
-
-
- )
-
-
-
- ≤
-
-
-
-
-
-
- λ
-
-
- f
-
-
- (
-
-
- x
-
-
- )
-
-
-
- +
-
-
-
+ class="strut"
+ style="height: 1.7936em; vertical-align: -0.686em;"
+ />
- (
-
-
- 1
-
-
-
- −
+
+
+
+
+
+
+
+ b
+
+
+
+
+
+
+
+
+
+
+
+ a
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`LaTeX Plugin should render inline LaTeX with $$\\n..\\n$$ syntax 1`] = `
+
+
+
+
+ latex:
+
+
+
-
- λ
+
+
+ f
+
+
+ (
+
+
+ λ
+
+
+ x
+
+
+
+ +
+
+
- )
+
+
+ (
+
+
+ 1
+
+
+
+ −
+
+
- f
+
+
+ λ
+
+
+ )
+
+
+ y
+
+
+ )
+
+
+
+ ≤
+
+
- (
+
+
+ λ
+
+
+ f
+
+
+ (
+
+
+ x
+
+
+ )
+
+
+
+ +
+
+
- y
+
+
+ (
+
+
+ 1
+
+
+
+ −
+
+
- )
+
+
+ λ
+
+
+ )
+
+
+ f
+
+
+ (
+
+
+ y
+
+
+ )
+
-
-
+
+
`;
@@ -1684,94 +1724,98 @@ exports[`LaTeX Plugin should render inline LaTeX with $..$ syntax 1`] = `
class="ant-x-markdown x-markdown"
style="direction: ltr;"
>
-
-
+
+
-
- E
-
-
-
- =
-
-
-
-
-
-
- m
+
+
+ E
+
+
+
+ =
+
+
+
- c
+ m
+ c
+
+
-
+
- 2
+
+ 2
+
@@ -1784,8 +1828,8 @@ exports[`LaTeX Plugin should render inline LaTeX with $..$ syntax 1`] = `
-
-
+
+
`;
@@ -1796,84 +1840,88 @@ exports[`LaTeX Plugin should render inline LaTeX with [\\n..\\n] syntax 1`] = `
class="ant-x-markdown x-markdown"
style="direction: ltr;"
>
-
- latex:
-
+
+
+ latex:
-
- L
-
+ class="strut"
+ style="height: 1.1413em; vertical-align: -0.25em;"
+ />
+ L
+
+
-
+
- C
-
-
- L
-
-
- I
-
-
- P
+
+ C
+
+
+ L
+
+
+ I
+
+
+ P
+
@@ -1882,124 +1930,101 @@ exports[`LaTeX Plugin should render inline LaTeX with [\\n..\\n] syntax 1`] = `
+
+ (
+
+
+ θ
+
+
+ )
+
+
+
+ =
+
+
- (
-
-
- θ
-
-
- )
-
-
-
- =
-
-
-
-
-
-
- E
-
+ class="strut"
+ style="height: 1.8em; vertical-align: -0.65em;"
+ />
+ E
+
+
-
+
- t
+
+ t
+
+
+
+
-
-
-
-
-
-
+
+
+
-
-
-
-
-
- [
-
-
-
- min
-
- (
+ [
+ min
+
+
+
- r
+
+ (
+
+ r
+
+
-
+
- t
+
+ t
+
+
+
+
-
-
-
-
-
-
+
+
+
-
-
- (
-
-
- θ
-
-
- )
-
-
+ (
+
+
+ θ
+
+
+ )
+
+
-
- A
+
+
+ A
+
-
-
-
+
- ^
+
+ ^
+
-
-
-
+
- t
+
+ t
+
+
+
+
-
-
-
-
-
-
+
+
+
-
-
- ,
-
-
-
- clip
+ ,
-
-
- (
-
-
+
+
+ clip
+
+
+
- r
+ (
+ r
+
+
-
+
- t
+
+ t
+
+
+
+
-
-
-
-
-
-
+
+
+
-
-
- (
-
-
- θ
-
-
- )
-
-
- ,
-
-
-
- 1
-
-
-
- −
-
-
-
- ϵ
-
-
- ,
-
-
-
- 1
-
-
-
- +
-
-
-
- ϵ
-
-
- )
-
-
+ (
+
+
+ θ
+
+
+ )
+
+
+ ,
+
+
+
+ 1
+
+
+
+ −
+
+
+
+ ϵ
+
+
+ ,
+
+
+
+ 1
+
+
+
+ +
+
+
+
+ ϵ
+
+
+ )
+
+
-
- A
+
+
+ A
+
-
-
-
+
- ^
+
+ ^
+
-
-
-
+
- t
+
+ t
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+ )
+
+
- )
+ ]
-
-
- ]
-
-
-
end
-
+
+
`;
diff --git a/packages/x/components/bubble/__tests__/__snapshots__/demo-extend.test.ts.snap b/packages/x/components/bubble/__tests__/__snapshots__/demo-extend.test.ts.snap
index 7b1696559..357034491 100644
--- a/packages/x/components/bubble/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/packages/x/components/bubble/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -3870,19 +3870,23 @@ exports[`renders components/bubble/demo/markdown.tsx extend context correctly 1`
class="ant-x-markdown x-markdown"
style="direction: ltr;"
>
-
+
+
+
+ Render as markdown content to show rich text!
+
+
- Render as markdown content to show rich text!
+ Link:
+
+ Ant Design X
+
-
-
- Link:
-
- Ant Design X
-
-
+
diff --git a/packages/x/docs/x-markdown/demo/codeDemo/footer.tsx b/packages/x/docs/x-markdown/demo/codeDemo/footer.tsx
new file mode 100644
index 000000000..dbc43844d
--- /dev/null
+++ b/packages/x/docs/x-markdown/demo/codeDemo/footer.tsx
@@ -0,0 +1,98 @@
+import { XMarkdown } from '@ant-design/x-markdown';
+import { Button, Card, Spin } from 'antd';
+import React, { useEffect, useRef, useState } from 'react';
+import { useIntl } from 'react-intl';
+
+const FooterDemo = () => {
+ const [content, setContent] = useState('');
+ const [loading, setLoading] = useState(true);
+ const timer = useRef | null>(null);
+ const { locale } = useIntl();
+
+ const text = `
+# AI Assistant Response
+
+Hello! I'm your AI assistant.
+
+I'm here to help you with various tasks.
+
+Let me provide you with a comprehensive answer about how footer works in streaming scenarios.
+
+## Key Features
+
+- **Loading Indicators**: Show processing status
+- **Completion Status**: Display when content is fully loaded
+- **Custom Styling**: Fully customizable appearance
+ `;
+
+ const startStreaming = () => {
+ setLoading(true);
+ setContent('');
+
+ let currentIndex = 0;
+ const streamInterval = setInterval(() => {
+ if (currentIndex <= text.length) {
+ setContent(text.slice(0, currentIndex));
+ currentIndex += 2;
+ } else {
+ clearInterval(streamInterval);
+ setLoading(false);
+ }
+ }, 30);
+
+ timer.current = streamInterval;
+ };
+
+ const handleReRender = () => {
+ if (timer.current) {
+ clearInterval(timer.current);
+ }
+ startStreaming();
+ };
+
+ useEffect(() => {
+ startStreaming();
+ return () => {
+ if (timer.current) {
+ clearInterval(timer.current);
+ }
+ };
+ }, []);
+
+ return (
+ Re-Render}
+ >
+
+
+ AI is thinking...
+
+ ) : (
+
+ ✨ Response completed · Generated by AI Assistant
+
+ )
+ }
+ streaming={{ hasNextChunk: loading }}
+ />
+
+ );
+};
+
+export default FooterDemo;
diff --git a/packages/x/docs/x-markdown/examples.en-US.md b/packages/x/docs/x-markdown/examples.en-US.md
index 66873aa60..e08e57a6f 100644
--- a/packages/x/docs/x-markdown/examples.en-US.md
+++ b/packages/x/docs/x-markdown/examples.en-US.md
@@ -14,6 +14,7 @@ Used for rendering streaming Markdown format returned by LLM.
+
@@ -32,6 +33,9 @@ Used for rendering streaming Markdown format returned by LLM.
| components | Custom React components to replace HTML elements | `Record \| keyof JSX.IntrinsicElements>`, see [details](/x-markdowns/components) | - |
| paragraphTag | Custom HTML tag for paragraph elements to prevent validation errors when custom components contain block-level elements | `keyof JSX.IntrinsicElements` | `'p'` |
| streaming | Configuration for streaming rendering behavior | `StreamingOption`, see [syntax processing](/x-markdowns/streaming-syntax) and [animation effects](/x-markdowns/streaming-animation) | - |
+| footer | React node displayed at the end | `React.ReactNode` | - |
+| footerClassName | The CSS class for the footer | `string` | - |
+| footerStyle | The styles for the footer | `CSSProperties` | - |
| config | Marked.js configuration for Markdown parsing and extensions | [`MarkedExtension`](https://marked.js.org/using_advanced#options) | `{ gfm: true }` |
| openLinksInNewTab | Whether to add `target="_blank"` to all a tags | `boolean` | `false` |
| dompurifyConfig | DOMPurify configuration for HTML sanitization and XSS protection | [`DOMPurify.Config`](https://github.com/cure53/DOMPurify#can-i-configure-dompurify) | - |
diff --git a/packages/x/docs/x-markdown/examples.zh-CN.md b/packages/x/docs/x-markdown/examples.zh-CN.md
index 0f849a935..1484c6650 100644
--- a/packages/x/docs/x-markdown/examples.zh-CN.md
+++ b/packages/x/docs/x-markdown/examples.zh-CN.md
@@ -14,6 +14,7 @@ order: 2
+
@@ -32,6 +33,9 @@ order: 2
| components | 用于替换 HTML 元素的自定义 React 组件 | `Record \| keyof JSX.IntrinsicElements>`,查看[详情](/x-markdowns/components-cn) | - |
| paragraphTag | 段落元素的自定义 HTML 标签,防止自定义组件包含块级元素时的验证错误 | `keyof JSX.IntrinsicElements` | `'p'` |
| streaming | 流式渲染行为的配置 | `StreamingOption`,查看[语法处理](/x-markdowns/streaming-syntax)和[动画效果](/x-markdowns/streaming-animation) | - |
+| footer | 显示在文档末尾的 React 节点 | `React.ReactNode` | - |
+| footerClassName | 为`footer`设置CSS类 | `string` | - |
+| footerStyle | 为`footer`设置内联样式 | `CSSProperties` | - |
| config | Markdown 解析和扩展的 Marked.js 配置 | [`MarkedExtension`](https://marked.js.org/using_advanced#options) | `{ gfm: true }` |
| openLinksInNewTab | 是否为所有 a 标签添加 `target="_blank"` | `boolean` | `false` |
| dompurifyConfig | HTML 净化和 XSS 防护的 DOMPurify 配置 | [`DOMPurify.Config`](https://github.com/cure53/DOMPurify#can-i-configure-dompurify) | - |