Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d84e551
feat(markdown): 初步接入cherryMarkdown
LzhengH Jun 3, 2025
de1e326
feat(markdown): 初步调整cherryMarkdown接入样式
LzhengH Jun 6, 2025
f6b8110
Merge branch 'develop' into feature/chatbot-cherry-markdown
LzhengH Jul 18, 2025
b5e4c7c
feat(cherryMarkdonw): chatMessage接入cherryMD,basic markdownRender示例跑通
LzhengH Jul 18, 2025
03a7c78
feat(cherryMarkdown): 添加自定义光标示例
LzhengH Jul 21, 2025
f92df6c
feat(cherryMarkdownb): 替换默认code渲染
LzhengH Jul 22, 2025
2a6fd8c
feat(cherrymarkdonw): 更新cherry版本,处理代码块全量替换
LzhengH Jul 23, 2025
d331deb
feat(cherry markdown): cherry添加part
LzhengH Jul 23, 2025
47a0f08
feat(cherryMarkdown): 去除markdown-it
LzhengH Aug 4, 2025
5203aa9
feat(cherryMarkdown): 完成公式异步加载
LzhengH Aug 4, 2025
ec08511
Merge branch 'develop' into feature/chatbot-cherry-markdown
LzhengH Aug 4, 2025
78ee808
fix(chat-item): fix style
LzhengH Aug 4, 2025
3005b70
Merge branch 'fix/chatbot-style' into feature/chatbot-cherry-markdown
LzhengH Aug 4, 2025
a43d4ba
feat: 清理依赖
LzhengH Aug 4, 2025
9f55a32
fix(chat-message): 修复stop和error状态影响已渲染内容问题,修复骨架屏loading无法渲染
LzhengH Aug 4, 2025
451f831
Merge branch 'fix/chatbot-style' into feature/chatbot-cherry-markdown
LzhengH Aug 4, 2025
ae3fe3d
feat: part调整
LzhengH Aug 4, 2025
8c684df
feat(cherry): 升级cherryMd,完成代码块替换功能
LzhengH Aug 5, 2025
a376a6d
docs(data): 恢复测试数据
LzhengH Aug 5, 2025
43397f3
feat(cherryMarkdown): upgrade cherry markdown
LzhengH Aug 12, 2025
aea4bdd
Merge branch 'develop' into feature/chatbot-cherry-markdown
LzhengH Sep 1, 2025
4712e69
feat(cherryMarkdown): 调整cherry添加part逻辑避免覆盖自定义语法part,调整示例为动态渲染,导出渲染引擎
LzhengH Sep 1, 2025
e6cd34e
feat(cherry): 处理cherryMD里自定义代码块参数问题
LzhengH Sep 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,669 changes: 1,472 additions & 197 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,14 @@
"dependencies": {
"@babel/runtime": "^7.24.7",
"@popperjs/core": "^2.11.8",
"@types/markdown-it": "^13.0.7",
"@vscode/markdown-it-katex": "^1.1.1",
"cherry-markdown": "^0.10.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"copy-to-clipboard": "^3.3.3",
"htm": "^3.1.1",
"immer": "^10.1.1",
"lodash-es": "^4.17.21",
"markdown-it": "^14.1.0",
"markdown-it-fence": "^0.1.3",
"markdown-it-link-attributes": "^4.0.1",
"nanostores": "^0.11.4",
"omi": "^7.7.13",
"omi-reactify": "0.0.10",
Expand Down
90 changes: 23 additions & 67 deletions src/chat-message/_example/markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +1,37 @@
import '../content/markdown-content';

import { Component } from 'omi';

const doc = `
# This is TDesign



## This is TDesign



### This is TDesign


#### This is TDesign


The point of reference-style links is not that they’re easier to write. The point is that with reference-style links, your document source is vastly more readable. Compare the above examples: using reference-style links, the paragraph itself is only 81 characters long; with inline-style links, it’s 176 characters; and as raw \`HTML\`, it’s 234 characters. In the raw \`HTML\`, there’s more markup than there is text.



> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet.



an example | *an example* | **an example**



1. Bird
1. McHale
1. Parish
1. Bird
1. McHale
1. Parish


- Red
- Green
- Blue
- Red
- Green
- Blue
import { Component, signal } from 'omi';

import mdContent from '../../chatbot/mock/testMarkdown.md?raw';

export default class BasicExample extends Component {
displayText = signal('');

This is [an example](http://example.com/ "Title") inline link.
isTyping = true;

<http://example.com/>
currentIndex = 0;

timer = null;

\`\`\`bash
$ npm i tdesign-vue-next
\`\`\`
typeEffect = () => {
if (!this.isTyping) return;

---
if (this.currentIndex < mdContent.length) {
const char = mdContent[this.currentIndex];
this.currentIndex += 1;
this.displayText.value += char;
this.timer = setTimeout(this.typeEffect, 20);
} else {
// 输入完成时自动停止
this.isTyping = false;
}
};

\`\`\`json
{
"prompt": "mage_url(generated image URL):mage_url(generated image URL):mage_url(generated image URL):mage_url(generated image URL):mage_url(generated image URL):"
}
\`\`\`
`;
ready(): void {
this.typeEffect();
}

export default class BasicExample extends Component {
render() {
return (
<t-chat-md-content
content={doc}
pluginConfig={[
{
preset: 'code',
enabled: true,
},
]}
></t-chat-md-content>
);
return <t-chat-md-content content={this.displayText.value} />;
}
}
193 changes: 70 additions & 123 deletions src/chat-message/content/markdown-content.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import MarkdownIt from 'markdown-it';
import linkPlugin from 'markdown-it-link-attributes';
import { Component, OmiProps, signal, tag } from 'omi';
import '../md/chat-md-code';

import Cherry from 'cherry-markdown/dist/cherry-markdown.core';
import type { CherryOptions } from 'cherry-markdown/types/cherry';
import { escape, merge } from 'lodash-es';
import { Component, createRef, signal, tag } from 'omi';

import { getClassPrefix } from '../../_util/classname';
import { setExportparts } from '../../_util/dom';
import markdownItCjFriendlyPlugin from '../md/markdownItCjkFriendly';
import { mdPartAttrPlugin } from '../md/utils';
import { AddPartHook } from '../md/utils';

import styles from '../style/chat-content.less';
// 单独用该组件时,发现动态加载样式不生效,目前直接引入
Expand All @@ -14,7 +16,7 @@ import codeStyles from '../style/md/chat-md-code.less';
const baseClass = `${getClassPrefix()}-chat__text`;

/** markdown插件预设 */
export type TdChatContentMDPresetPlugin = 'code' | 'link' | 'katex';
export type TdChatContentMDPresetPlugin = 'katex';

export interface TdChatContentMDPresetConfig {
preset: TdChatContentMDPresetPlugin;
Expand All @@ -26,146 +28,90 @@ export interface TdChatContentMDPresetConfig {

export type TdChatContentMDPluginConfig =
/** 预设插件配置 */
| TdChatContentMDPresetConfig
/** markdownIt原生插件配置 */
| MarkdownIt.PluginSimple
| MarkdownIt.PluginWithParams
| MarkdownIt.PluginWithOptions;
TdChatContentMDPresetConfig;

export type TdChatContentMDOptions = Omit<CherryOptions, 'id' | 'el' | 'toolbars'>;

export interface TdChatMarkdownContentProps {
content?: string;
options?: MarkdownIt.Options;
pluginConfig?: Array<TdChatContentMDPluginConfig>;
options?: TdChatContentMDOptions;
}

@tag('t-chat-md-content')
export default class ChatMDContent extends Component<TdChatMarkdownContentProps> {
export default class ChatCherryMDContent extends Component<TdChatMarkdownContentProps> {
static css = [styles, codeStyles];

static propTypes = {
content: String,
options: Object,
pluginConfig: Object,
};

static defaultProps = {
// TODO: 现在是测效果,正式看下默认怎么配。
options: {
html: true, // 允许HTML标签
breaks: true, // 自动换行
typographer: true, // 排版优化
},
pluginConfig: [
{
preset: 'code',
enabled: false,
},
{
preset: 'katex',
enabled: false,
},
],
static defaultProps: Partial<TdChatMarkdownContentProps> = {
options: {},
};

// TODO: md对象看看是不是直接provider传进来,否则每个content都要构造一个
md: MarkdownIt | null = null;
mdRef = createRef<HTMLElement>();

md: Cherry | null = null;

isMarkdownInit = signal(false);

receiveProps(
props: TdChatMarkdownContentProps | OmiProps<TdChatMarkdownContentProps, any>,
oldProps: TdChatMarkdownContentProps | OmiProps<TdChatMarkdownContentProps, any>,
) {
if (props.content?.length === oldProps.content?.length) {
return false;
}
return true;
}
/** 传入cherryMarkdown的配置 */
private markdownOptions: CherryOptions = {
engine: {
global: {
flowSessionContext: true,
},
syntax: {
table: {
selfClosing: true,
},
link: {
target: '_blank',
},
codeBlock: {
customRenderer: {
// 自定义语法渲染器
all: {
render: (code, _sign, _cherry, lang) =>
`<t-chat-md-code key="${_sign}" data-lang="${lang}" data-code="${escape(code)}" />`,
},
},
},
},
customSyntax: {
AddPart: {
syntaxClass: AddPartHook,
before: 'frontMatter',
},
},
},
toolbars: {
toolbar: false,
toc: false,
showToolbar: false,
},
editor: {
defaultModel: 'previewOnly',
},
previewer: {
enablePreviewerBubble: false,
},
};

ready() {
const { options } = this.props;
this.markdownOptions = merge(this.markdownOptions, options);
this.initMarkdown();
setExportparts(this);
}

initMarkdown = async () => {
const { options, pluginConfig } = this.props;

this.isMarkdownInit.value = false;
const md = MarkdownIt({
...options,
})
.use(markdownItCjFriendlyPlugin)
.use(mdPartAttrPlugin)
// 表格
.use((md) => {
md.renderer.rules.table_open = () =>
`<div class=${baseClass}__markdown__table__wrapper part=${baseClass}__markdown__table__wrapper>\n<table>\n`;
md.renderer.rules.table_close = () => '</table>\n</div>';
})
.use(linkPlugin, {
attrs: {
target: '_blank',
rel: 'noopener',
},
});

// 筛选生效的预设插件
const enabledPresetPlugins =
(pluginConfig?.filter((item) => {
if (typeof item !== 'object') {
return false;
}
if (typeof item.enabled === 'boolean' && !item.enabled) {
return false;
}
return true;
}) as TdChatContentMDPresetConfig[] | undefined) || [];
// 筛选自定义插件
const customPlugins =
(pluginConfig?.filter((item) => typeof item !== 'object') as Array<
Exclude<TdChatContentMDPluginConfig, TdChatContentMDPresetConfig>
>) || [];

// 注入预设插件
// 代码块
const codeHighlightConfig = enabledPresetPlugins.find((item) => item.preset === 'code');
if (codeHighlightConfig) {
await import('../md/chat-md-code');

const codeHighlight = (code: string, lang: string, attrs: string) => {
// 优先取用户自定义代码块渲染
const customHighlight = options?.highlight?.(code, lang, attrs);
if (customHighlight) {
return customHighlight;
}
// 传参注意转义
return `<t-chat-md-code lang="${lang}" code="${md.utils.escapeHtml(code)}"></t-chat-md-code>`;
};
md.options.highlight = codeHighlight;
}

// 其他预设
for await (const config of enabledPresetPlugins) {
const { preset, options } = config;
switch (preset) {
// 链接
case 'link': {
const plugin = await import('markdown-it-link-attributes');
md.use(plugin.default, options);
break;
}
// 公式
case 'katex': {
const plugin = await import('@vscode/markdown-it-katex');
md.use(plugin.default, options);
break;
}
}
}

// 注入自定义插件
customPlugins.forEach((plugin) => {
md.use(plugin);

const md = new Cherry({
...this.markdownOptions,
el: this.mdRef.current,
});

this.md = md;
Expand All @@ -180,14 +126,15 @@ export default class ChatMDContent extends Component<TdChatMarkdownContentProps>

parseMarkdown(markdown: string) {
if (!this.isMarkdownInit.value || !markdown) return '';
return this.md?.render(markdown);
return this.md?.setMarkdown(markdown);
}

render() {
const html = this.getTextInfo();
this.getTextInfo();

return (
<div className={`${baseClass}`}>
{!!html && <div className={`${baseClass}__markdown`} unsafeHTML={{ html }}></div>}
<div ref={this.mdRef} className={`${baseClass}__markdown`}></div>
</div>
);
}
Expand Down
4 changes: 4 additions & 0 deletions src/chat-message/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Cherry from 'cherry-markdown/dist/cherry-markdown.core';

import _MarkdownContent from './content/markdown-content';
import _SearchContent from './content/search-content';
import _SuggestionContent from './content/suggestion-content';
Expand All @@ -9,6 +11,8 @@ export const ChatSuggestionContent = _SuggestionContent;
export const ChatMarkdownContent = _MarkdownContent;
export const ChatSearchContent = _SearchContent;
export const ChatMessage = _ChatItem;
// 外部使用自定义逻辑时使用的CherryMarkdown类必须跟渲染时保持一致
export const TdMarkdownEngine = Cherry;

export type { TdChatThinkContentProps } from './content/thinking-content';
export type { TdChatSearchContentProps } from './content/search-content';
Expand Down
Loading