@alifd/chat 0.3.32-beta.5 → 0.3.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -103,7 +103,7 @@ const HTMLRenderer = memo(function HTMLRenderer({ className, children, imagePrev
103
103
  const parserOptions = {
104
104
  // @ts-ignore
105
105
  replace: domNode => {
106
- var _a, _b, _c, _d, _e, _f, _g;
106
+ var _a, _b, _c, _d, _e, _f;
107
107
  // 处理文本节点
108
108
  if ((domNode.type === 'text' || domNode.nodeType === 3) && typewriterEffect) {
109
109
  return processTextNode(domNode);
@@ -112,22 +112,10 @@ const HTMLRenderer = memo(function HTMLRenderer({ className, children, imagePrev
112
112
  const { name } = domNode;
113
113
  if (name === 'link-reference') {
114
114
  const element = (domToReact([domNode]));
115
- // 检查是否有子元素
116
- if (!((_a = element.props) === null || _a === void 0 ? void 0 : _a.children)) {
117
- return null;
118
- }
119
115
  // link-reference 的子元素一定是 a 标签
120
- const aElement = (_b = element.props) === null || _b === void 0 ? void 0 : _b.children;
121
- // 检查a标签是否存在
122
- if (!aElement || !aElement.props) {
123
- return null;
124
- }
116
+ const aElement = (_a = element.props) === null || _a === void 0 ? void 0 : _a.children;
125
117
  // a 标签的子元素一定是 span 节点
126
- const spanElement = (_c = aElement === null || aElement === void 0 ? void 0 : aElement.props) === null || _c === void 0 ? void 0 : _c.children;
127
- // 检查span元素是否存在
128
- if (!spanElement || !spanElement.props) {
129
- return null;
130
- }
118
+ const spanElement = (_b = aElement === null || aElement === void 0 ? void 0 : aElement.props) === null || _b === void 0 ? void 0 : _b.children;
131
119
  const handleUrlClick = () => {
132
120
  var _a;
133
121
  if (!((_a = aElement === null || aElement === void 0 ? void 0 : aElement.props) === null || _a === void 0 ? void 0 : _a.href)) {
@@ -140,10 +128,10 @@ const HTMLRenderer = memo(function HTMLRenderer({ className, children, imagePrev
140
128
  defaultOpenLink(aElement.props.href);
141
129
  }
142
130
  };
143
- return (React.createElement(Balloon, { v2: true, align: "b", className: "link-reference-balloon", closable: false, offset: [0, -8], triggerType: ['hover'], trigger: React.createElement("span", { className: "link-reference-index", onClick: handleUrlClick }, (_d = spanElement === null || spanElement === void 0 ? void 0 : spanElement.props) === null || _d === void 0 ? void 0 : _d.children) },
131
+ return (React.createElement(Balloon, { v2: true, align: "b", className: "link-reference-balloon", closable: false, offset: [0, -8], triggerType: ['hover'], trigger: React.createElement("span", { className: "link-reference-index", onClick: handleUrlClick }, (_c = spanElement === null || spanElement === void 0 ? void 0 : spanElement.props) === null || _c === void 0 ? void 0 : _c.children) },
144
132
  React.createElement("div", { className: "link-reference-content" },
145
- React.createElement("img", { className: "link-reference-source-icon", src: ((_e = element.props) === null || _e === void 0 ? void 0 : _e['data-source-icon']) || '' }),
146
- React.createElement("a", { className: "link-reference-title", onClick: handleUrlClick }, ((_f = element.props) === null || _f === void 0 ? void 0 : _f['data-title']) || ''))));
133
+ React.createElement("img", { className: "link-reference-source-icon", src: ((_d = element.props) === null || _d === void 0 ? void 0 : _d['data-source-icon']) || '' }),
134
+ React.createElement("a", { className: "link-reference-title", onClick: handleUrlClick }, ((_e = element.props) === null || _e === void 0 ? void 0 : _e['data-title']) || ''))));
147
135
  }
148
136
  if (name === 'a') {
149
137
  const element = (domToReact([domNode]));
@@ -197,7 +185,7 @@ const HTMLRenderer = memo(function HTMLRenderer({ className, children, imagePrev
197
185
  return renderImage(element.props);
198
186
  }
199
187
  // 换成统一的图片渲染
200
- return React.createElement(Img, Object.assign({}, element.props, { imageClassName: (_g = element.props) === null || _g === void 0 ? void 0 : _g.className, enablePreview: imagePreview, onImageClick: () => {
188
+ return React.createElement(Img, Object.assign({}, element.props, { imageClassName: (_f = element.props) === null || _f === void 0 ? void 0 : _f.className, enablePreview: imagePreview, onImageClick: () => {
201
189
  handleImageClick === null || handleImageClick === void 0 ? void 0 : handleImageClick(element.props.src);
202
190
  } }));
203
191
  }
package/es/index.js CHANGED
@@ -31,4 +31,4 @@ export { default as RadioGroup } from './radio-group';
31
31
  export { default as CheckboxGroup } from './checkbox-group';
32
32
  export { default as Select } from './select';
33
33
  export { default as Flip } from './flip';
34
- export const version = '0.3.32-beta.5';
34
+ export const version = '0.3.32';
@@ -10,6 +10,8 @@ import hljs from 'highlight.js'; // 引入 highlight.js
10
10
  import 'highlight.js/styles/github.css'; // 引入代码高亮样式
11
11
  let displayedContentCache = '';
12
12
  const DEFAULT_LOADING_ICON = 'https://img.alicdn.com/imgextra/i1/O1CN01wdlADU1WCvOLNSB3w_!!6000000002753-1-tps-530-255.gif';
13
+ // 默认白名单标签
14
+ const DEFAULT_WHITELIST_TAGS = ['img', 'code', 'pre', 'link-reference'];
13
15
  /**
14
16
  * @component Markdown
15
17
  * @en Markdown
@@ -26,8 +28,37 @@ const Markdown = forwardRef((_a, ref) => {
26
28
  }, [onReady]);
27
29
  const containerRef = useRef(null);
28
30
  const [displayedContent, setDisplayedContent] = useState(() => (others === null || others === void 0 ? void 0 : others.typewriterEffect) ? displayedContentCache : content);
29
- const [contentQueue, setContentQueue] = useState(() => (others === null || others === void 0 ? void 0 : others.typewriterEffect) ? content.split('') : []);
31
+ const [contentQueue, setContentQueue] = useState(() => (others === null || others === void 0 ? void 0 : others.typewriterEffect) ? createContentQueue(content, others.whitelistTags) : []);
30
32
  const typeTimeoutRef = useRef(null);
33
+ // 创建打字机内容队列,处理白名单标签
34
+ function createContentQueue(text, customWhitelist) {
35
+ if (!text)
36
+ return [];
37
+ const whitelist = [...DEFAULT_WHITELIST_TAGS, ...(customWhitelist || [])];
38
+ if (whitelist.length === 0) {
39
+ return text.split('');
40
+ }
41
+ // 创建匹配白名单标签的正则表达式
42
+ const tagPattern = new RegExp(`(<(${whitelist.join('|')}).*?>.*?<\\/\\2>)`, 'gs');
43
+ const parts = [];
44
+ let lastIndex = 0;
45
+ let match;
46
+ // 分割文本,将白名单标签作为整体,其他文本按字符分割
47
+ while ((match = tagPattern.exec(text)) !== null) {
48
+ if (match.index > lastIndex) {
49
+ // 添加标签前的普通文本,按字符分割
50
+ parts.push(...text.substring(lastIndex, match.index).split(''));
51
+ }
52
+ // 添加整个标签内容作为一项
53
+ parts.push(match[1]);
54
+ lastIndex = match.index + match[1].length;
55
+ }
56
+ // 添加剩余的普通文本,按字符分割
57
+ if (lastIndex < text.length) {
58
+ parts.push(...text.substring(lastIndex).split(''));
59
+ }
60
+ return parts;
61
+ }
31
62
  // 清理定时器函数
32
63
  const clearTypeTimer = () => {
33
64
  if (typeTimeoutRef.current) {
@@ -60,9 +91,9 @@ const Markdown = forwardRef((_a, ref) => {
60
91
  const displayPart = content.slice(0, contentLength - diffLength);
61
92
  const typingPart = content.slice(contentLength - diffLength);
62
93
  setDisplayedContent(displayPart);
63
- setContentQueue(typingPart.split(''));
94
+ setContentQueue(createContentQueue(typingPart, others.whitelistTags));
64
95
  }
65
- }, [content, others === null || others === void 0 ? void 0 : others.typewriterEffect]);
96
+ }, [content, others === null || others === void 0 ? void 0 : others.typewriterEffect, others === null || others === void 0 ? void 0 : others.whitelistTags]);
66
97
  // 执行打字机效果 - 仅负责处理contentQueue中的字符
67
98
  useEffect(() => {
68
99
  // 如果没有启用打字机效果或没有内容需要打字
@@ -71,25 +102,25 @@ const Markdown = forwardRef((_a, ref) => {
71
102
  return;
72
103
  }
73
104
  // 固定吐字速度为50ms一次
74
- const timePerChar = 100;
75
- // 动态调整一次吐字的字数,确保1s内吐完,最少吐2个字符
76
- const charsPerTime = Math.max(Math.floor(contentQueue.length / 10), 2);
77
- const processNextChar = () => {
105
+ const timePerItem = 100;
106
+ // 动态调整一次吐字的字数,确保1s内吐完,最少吐2个列表项
107
+ const itemsPerTime = Math.max(Math.floor(contentQueue.length / 10), 2);
108
+ const processNextItem = () => {
78
109
  const currentQueue = contentQueue;
79
110
  if (currentQueue.length === 0) {
80
111
  return;
81
112
  }
82
- // 获取并移除队列中的第charsPerTime个字符
83
- const nextChars = currentQueue.slice(0, charsPerTime);
84
- const newQueue = currentQueue.slice(charsPerTime);
113
+ // 获取并移除队列中的第charsPerTime个列表项
114
+ const nextItems = currentQueue.slice(0, itemsPerTime);
115
+ const newQueue = currentQueue.slice(itemsPerTime);
85
116
  // 更新显示内容
86
- setDisplayedContent(prev => prev + nextChars.join(''));
117
+ setDisplayedContent(prev => prev + nextItems.join(''));
87
118
  // 更新队列
88
119
  setContentQueue(newQueue);
89
120
  };
90
121
  // 开始处理第一个字符
91
122
  clearTypeTimer(); // 确保没有并发定时器
92
- typeTimeoutRef.current = setTimeout(processNextChar, timePerChar);
123
+ typeTimeoutRef.current = setTimeout(processNextItem, timePerItem);
93
124
  return clearTypeTimer;
94
125
  }, [contentQueue.length, others === null || others === void 0 ? void 0 : others.typewriterEffect]);
95
126
  // 处理 HTML 标签
@@ -71,4 +71,9 @@ export interface MarkdownProps extends React.HTMLAttributes<HTMLElement> {
71
71
  * 是否启用打字机效果
72
72
  */
73
73
  typewriterEffect?: boolean;
74
+ /**
75
+ * 白名单标签列表,这些标签及其内容会作为整体放入打字机队列
76
+ * @en Whitelist tags that will be put into typewriter queue as a whole
77
+ */
78
+ whitelistTags?: string[];
74
79
  }
@@ -106,7 +106,7 @@ const HTMLRenderer = (0, react_1.memo)(function HTMLRenderer({ className, childr
106
106
  const parserOptions = {
107
107
  // @ts-ignore
108
108
  replace: domNode => {
109
- var _a, _b, _c, _d, _e, _f, _g;
109
+ var _a, _b, _c, _d, _e, _f;
110
110
  // 处理文本节点
111
111
  if ((domNode.type === 'text' || domNode.nodeType === 3) && typewriterEffect) {
112
112
  return processTextNode(domNode);
@@ -115,22 +115,10 @@ const HTMLRenderer = (0, react_1.memo)(function HTMLRenderer({ className, childr
115
115
  const { name } = domNode;
116
116
  if (name === 'link-reference') {
117
117
  const element = ((0, html_react_parser_1.domToReact)([domNode]));
118
- // 检查是否有子元素
119
- if (!((_a = element.props) === null || _a === void 0 ? void 0 : _a.children)) {
120
- return null;
121
- }
122
118
  // link-reference 的子元素一定是 a 标签
123
- const aElement = (_b = element.props) === null || _b === void 0 ? void 0 : _b.children;
124
- // 检查a标签是否存在
125
- if (!aElement || !aElement.props) {
126
- return null;
127
- }
119
+ const aElement = (_a = element.props) === null || _a === void 0 ? void 0 : _a.children;
128
120
  // a 标签的子元素一定是 span 节点
129
- const spanElement = (_c = aElement === null || aElement === void 0 ? void 0 : aElement.props) === null || _c === void 0 ? void 0 : _c.children;
130
- // 检查span元素是否存在
131
- if (!spanElement || !spanElement.props) {
132
- return null;
133
- }
121
+ const spanElement = (_b = aElement === null || aElement === void 0 ? void 0 : aElement.props) === null || _b === void 0 ? void 0 : _b.children;
134
122
  const handleUrlClick = () => {
135
123
  var _a;
136
124
  if (!((_a = aElement === null || aElement === void 0 ? void 0 : aElement.props) === null || _a === void 0 ? void 0 : _a.href)) {
@@ -143,10 +131,10 @@ const HTMLRenderer = (0, react_1.memo)(function HTMLRenderer({ className, childr
143
131
  defaultOpenLink(aElement.props.href);
144
132
  }
145
133
  };
146
- return (react_1.default.createElement(balloon_1.default, { v2: true, align: "b", className: "link-reference-balloon", closable: false, offset: [0, -8], triggerType: ['hover'], trigger: react_1.default.createElement("span", { className: "link-reference-index", onClick: handleUrlClick }, (_d = spanElement === null || spanElement === void 0 ? void 0 : spanElement.props) === null || _d === void 0 ? void 0 : _d.children) },
134
+ return (react_1.default.createElement(balloon_1.default, { v2: true, align: "b", className: "link-reference-balloon", closable: false, offset: [0, -8], triggerType: ['hover'], trigger: react_1.default.createElement("span", { className: "link-reference-index", onClick: handleUrlClick }, (_c = spanElement === null || spanElement === void 0 ? void 0 : spanElement.props) === null || _c === void 0 ? void 0 : _c.children) },
147
135
  react_1.default.createElement("div", { className: "link-reference-content" },
148
- react_1.default.createElement("img", { className: "link-reference-source-icon", src: ((_e = element.props) === null || _e === void 0 ? void 0 : _e['data-source-icon']) || '' }),
149
- react_1.default.createElement("a", { className: "link-reference-title", onClick: handleUrlClick }, ((_f = element.props) === null || _f === void 0 ? void 0 : _f['data-title']) || ''))));
136
+ react_1.default.createElement("img", { className: "link-reference-source-icon", src: ((_d = element.props) === null || _d === void 0 ? void 0 : _d['data-source-icon']) || '' }),
137
+ react_1.default.createElement("a", { className: "link-reference-title", onClick: handleUrlClick }, ((_e = element.props) === null || _e === void 0 ? void 0 : _e['data-title']) || ''))));
150
138
  }
151
139
  if (name === 'a') {
152
140
  const element = ((0, html_react_parser_1.domToReact)([domNode]));
@@ -200,7 +188,7 @@ const HTMLRenderer = (0, react_1.memo)(function HTMLRenderer({ className, childr
200
188
  return renderImage(element.props);
201
189
  }
202
190
  // 换成统一的图片渲染
203
- return react_1.default.createElement(img_1.default, Object.assign({}, element.props, { imageClassName: (_g = element.props) === null || _g === void 0 ? void 0 : _g.className, enablePreview: imagePreview, onImageClick: () => {
191
+ return react_1.default.createElement(img_1.default, Object.assign({}, element.props, { imageClassName: (_f = element.props) === null || _f === void 0 ? void 0 : _f.className, enablePreview: imagePreview, onImageClick: () => {
204
192
  handleImageClick === null || handleImageClick === void 0 ? void 0 : handleImageClick(element.props.src);
205
193
  } }));
206
194
  }
package/lib/index.js CHANGED
@@ -68,4 +68,4 @@ var select_1 = require("./select");
68
68
  Object.defineProperty(exports, "Select", { enumerable: true, get: function () { return tslib_1.__importDefault(select_1).default; } });
69
69
  var flip_1 = require("./flip");
70
70
  Object.defineProperty(exports, "Flip", { enumerable: true, get: function () { return tslib_1.__importDefault(flip_1).default; } });
71
- exports.version = '0.3.32-beta.5';
71
+ exports.version = '0.3.32';
@@ -12,6 +12,8 @@ const highlight_js_1 = tslib_1.__importDefault(require("highlight.js")); // 引
12
12
  require("highlight.js/styles/github.css"); // 引入代码高亮样式
13
13
  let displayedContentCache = '';
14
14
  const DEFAULT_LOADING_ICON = 'https://img.alicdn.com/imgextra/i1/O1CN01wdlADU1WCvOLNSB3w_!!6000000002753-1-tps-530-255.gif';
15
+ // 默认白名单标签
16
+ const DEFAULT_WHITELIST_TAGS = ['img', 'code', 'pre', 'link-reference'];
15
17
  /**
16
18
  * @component Markdown
17
19
  * @en Markdown
@@ -28,8 +30,37 @@ const Markdown = (0, react_1.forwardRef)((_a, ref) => {
28
30
  }, [onReady]);
29
31
  const containerRef = (0, react_1.useRef)(null);
30
32
  const [displayedContent, setDisplayedContent] = (0, react_1.useState)(() => (others === null || others === void 0 ? void 0 : others.typewriterEffect) ? displayedContentCache : content);
31
- const [contentQueue, setContentQueue] = (0, react_1.useState)(() => (others === null || others === void 0 ? void 0 : others.typewriterEffect) ? content.split('') : []);
33
+ const [contentQueue, setContentQueue] = (0, react_1.useState)(() => (others === null || others === void 0 ? void 0 : others.typewriterEffect) ? createContentQueue(content, others.whitelistTags) : []);
32
34
  const typeTimeoutRef = (0, react_1.useRef)(null);
35
+ // 创建打字机内容队列,处理白名单标签
36
+ function createContentQueue(text, customWhitelist) {
37
+ if (!text)
38
+ return [];
39
+ const whitelist = [...DEFAULT_WHITELIST_TAGS, ...(customWhitelist || [])];
40
+ if (whitelist.length === 0) {
41
+ return text.split('');
42
+ }
43
+ // 创建匹配白名单标签的正则表达式
44
+ const tagPattern = new RegExp(`(<(${whitelist.join('|')}).*?>.*?<\\/\\2>)`, 'gs');
45
+ const parts = [];
46
+ let lastIndex = 0;
47
+ let match;
48
+ // 分割文本,将白名单标签作为整体,其他文本按字符分割
49
+ while ((match = tagPattern.exec(text)) !== null) {
50
+ if (match.index > lastIndex) {
51
+ // 添加标签前的普通文本,按字符分割
52
+ parts.push(...text.substring(lastIndex, match.index).split(''));
53
+ }
54
+ // 添加整个标签内容作为一项
55
+ parts.push(match[1]);
56
+ lastIndex = match.index + match[1].length;
57
+ }
58
+ // 添加剩余的普通文本,按字符分割
59
+ if (lastIndex < text.length) {
60
+ parts.push(...text.substring(lastIndex).split(''));
61
+ }
62
+ return parts;
63
+ }
33
64
  // 清理定时器函数
34
65
  const clearTypeTimer = () => {
35
66
  if (typeTimeoutRef.current) {
@@ -62,9 +93,9 @@ const Markdown = (0, react_1.forwardRef)((_a, ref) => {
62
93
  const displayPart = content.slice(0, contentLength - diffLength);
63
94
  const typingPart = content.slice(contentLength - diffLength);
64
95
  setDisplayedContent(displayPart);
65
- setContentQueue(typingPart.split(''));
96
+ setContentQueue(createContentQueue(typingPart, others.whitelistTags));
66
97
  }
67
- }, [content, others === null || others === void 0 ? void 0 : others.typewriterEffect]);
98
+ }, [content, others === null || others === void 0 ? void 0 : others.typewriterEffect, others === null || others === void 0 ? void 0 : others.whitelistTags]);
68
99
  // 执行打字机效果 - 仅负责处理contentQueue中的字符
69
100
  (0, react_1.useEffect)(() => {
70
101
  // 如果没有启用打字机效果或没有内容需要打字
@@ -73,25 +104,25 @@ const Markdown = (0, react_1.forwardRef)((_a, ref) => {
73
104
  return;
74
105
  }
75
106
  // 固定吐字速度为50ms一次
76
- const timePerChar = 100;
77
- // 动态调整一次吐字的字数,确保1s内吐完,最少吐2个字符
78
- const charsPerTime = Math.max(Math.floor(contentQueue.length / 10), 2);
79
- const processNextChar = () => {
107
+ const timePerItem = 100;
108
+ // 动态调整一次吐字的字数,确保1s内吐完,最少吐2个列表项
109
+ const itemsPerTime = Math.max(Math.floor(contentQueue.length / 10), 2);
110
+ const processNextItem = () => {
80
111
  const currentQueue = contentQueue;
81
112
  if (currentQueue.length === 0) {
82
113
  return;
83
114
  }
84
- // 获取并移除队列中的第charsPerTime个字符
85
- const nextChars = currentQueue.slice(0, charsPerTime);
86
- const newQueue = currentQueue.slice(charsPerTime);
115
+ // 获取并移除队列中的第charsPerTime个列表项
116
+ const nextItems = currentQueue.slice(0, itemsPerTime);
117
+ const newQueue = currentQueue.slice(itemsPerTime);
87
118
  // 更新显示内容
88
- setDisplayedContent(prev => prev + nextChars.join(''));
119
+ setDisplayedContent(prev => prev + nextItems.join(''));
89
120
  // 更新队列
90
121
  setContentQueue(newQueue);
91
122
  };
92
123
  // 开始处理第一个字符
93
124
  clearTypeTimer(); // 确保没有并发定时器
94
- typeTimeoutRef.current = setTimeout(processNextChar, timePerChar);
125
+ typeTimeoutRef.current = setTimeout(processNextItem, timePerItem);
95
126
  return clearTypeTimer;
96
127
  }, [contentQueue.length, others === null || others === void 0 ? void 0 : others.typewriterEffect]);
97
128
  // 处理 HTML 标签
@@ -71,4 +71,9 @@ export interface MarkdownProps extends React.HTMLAttributes<HTMLElement> {
71
71
  * 是否启用打字机效果
72
72
  */
73
73
  typewriterEffect?: boolean;
74
+ /**
75
+ * 白名单标签列表,这些标签及其内容会作为整体放入打字机队列
76
+ * @en Whitelist tags that will be put into typewriter queue as a whole
77
+ */
78
+ whitelistTags?: string[];
74
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alifd/chat",
3
- "version": "0.3.32-beta.5",
3
+ "version": "0.3.32",
4
4
  "description": "A configurable component library for chat built on React.",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.js",