@huyooo/ai-chat-shared 0.2.15 → 0.2.16

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huyooo/ai-chat-shared",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "description": "AI Chat 共享模块 - 内容解析器、类型定义",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -16,8 +16,7 @@
16
16
  "./styles": "./dist/styles.css"
17
17
  },
18
18
  "files": [
19
- "dist",
20
- "src"
19
+ "dist"
21
20
  ],
22
21
  "scripts": {
23
22
  "build": "tsup",
@@ -1,80 +0,0 @@
1
- /**
2
- * 代码高亮工具
3
- * 使用 highlight.js 提供代码语法高亮
4
- */
5
-
6
- import hljs from 'highlight.js'
7
-
8
- /** 支持的语言别名映射 */
9
- const LANGUAGE_ALIASES: Record<string, string> = {
10
- js: 'javascript',
11
- ts: 'typescript',
12
- tsx: 'typescript',
13
- jsx: 'javascript',
14
- py: 'python',
15
- rb: 'ruby',
16
- sh: 'bash',
17
- shell: 'bash',
18
- yml: 'yaml',
19
- md: 'markdown',
20
- }
21
-
22
- /**
23
- * 高亮代码
24
- * @param code 代码内容
25
- * @param language 语言标识
26
- * @returns 高亮后的 HTML
27
- */
28
- export function highlightCode(code: string, language?: string): string {
29
- if (!language) {
30
- // 无语言指定,尝试自动检测
31
- try {
32
- const result = hljs.highlightAuto(code)
33
- return result.value
34
- } catch {
35
- return escapeHtml(code)
36
- }
37
- }
38
-
39
- // 处理语言别名
40
- const normalizedLang = LANGUAGE_ALIASES[language.toLowerCase()] || language.toLowerCase()
41
-
42
- try {
43
- if (hljs.getLanguage(normalizedLang)) {
44
- return hljs.highlight(code, { language: normalizedLang }).value
45
- }
46
- // 语言不支持,返回转义后的原文
47
- return escapeHtml(code)
48
- } catch {
49
- return escapeHtml(code)
50
- }
51
- }
52
-
53
- /** HTML 转义 */
54
- function escapeHtml(text: string): string {
55
- return text
56
- .replace(/&/g, '&amp;')
57
- .replace(/</g, '&lt;')
58
- .replace(/>/g, '&gt;')
59
- .replace(/"/g, '&quot;')
60
- .replace(/'/g, '&#039;')
61
- }
62
-
63
- /**
64
- * 获取语言显示名称
65
- * @param language 语言标识
66
- * @returns 显示名称
67
- */
68
- export function getLanguageDisplayName(language?: string): string {
69
- if (!language) return 'Plain Text'
70
-
71
- const normalizedLang = LANGUAGE_ALIASES[language.toLowerCase()] || language.toLowerCase()
72
- const langDef = hljs.getLanguage(normalizedLang)
73
-
74
- if (langDef) {
75
- // 首字母大写
76
- return normalizedLang.charAt(0).toUpperCase() + normalizedLang.slice(1)
77
- }
78
-
79
- return language
80
- }
package/src/index.ts DELETED
@@ -1,59 +0,0 @@
1
- /**
2
- * @huyooo/ai-chat-shared
3
- *
4
- * AI Chat 共享模块
5
- * - 内容解析器
6
- * - 代码高亮
7
- * - 共享类型定义
8
- */
9
-
10
- // 类型导出
11
- export type {
12
- // 内容块类型
13
- ContentBlockType,
14
- ContentBlockBase,
15
- TextBlock,
16
- CodeBlock,
17
- ContentBlock,
18
- // 内置数据类型
19
- WeatherData,
20
- WeatherForecastItem,
21
- SearchResultItem,
22
- CodeExecutionResult,
23
- FileOperationResult,
24
- } from './types'
25
-
26
- // 解析器导出
27
- export {
28
- parseContent,
29
- createStreamParseState,
30
- parseContentStream,
31
- finishStreamParse,
32
- } from './parser'
33
- export type { StreamParseState } from './parser'
34
-
35
- // 高亮工具导出
36
- export {
37
- highlightCode,
38
- getLanguageDisplayName,
39
- } from './highlighter'
40
-
41
- // Markdown 渲染工具导出
42
- export {
43
- renderMarkdown,
44
- initMermaid,
45
- renderMermaidDiagrams,
46
- encodeMermaidCodeToBase64Url,
47
- renderLatexBlockToHtml,
48
- } from './markdown'
49
-
50
- // Mermaid / LaTeX 可视化渲染工具
51
- export {
52
- isMermaidLanguage,
53
- isLatexLanguage,
54
- isLatexDocument,
55
- canVisualizeLatex,
56
- hasLatexDelimiters,
57
- shouldShowVisualToggle,
58
- renderFencedLatexToHtml,
59
- } from './visual'
package/src/markdown.ts DELETED
@@ -1,335 +0,0 @@
1
- /**
2
- * Markdown 渲染工具
3
- * 提供统一的 Markdown 渲染函数,供 React 和 Vue 版本使用
4
- * 支持 LaTeX 数学公式渲染和 Mermaid 图表
5
- */
6
-
7
- import { marked, Renderer } from 'marked'
8
- import DOMPurify from 'dompurify'
9
- import katex from 'katex'
10
- import mermaid from 'mermaid'
11
- import { highlightCode } from './highlighter'
12
-
13
- // Mermaid 初始化标记
14
- let mermaidInitialized = false
15
-
16
- /**
17
- * base64url 编码(UTF-8)
18
- * 不能直接把源码放进 attribute(换行会被规范化),所以使用 base64 传递。
19
- * 同时避免在某些链路里 '+' 被当成空格的问题,这里使用 base64url(- _,无 padding)。
20
- */
21
- function encodeBase64Utf8(text: string): string {
22
- const bytes = new TextEncoder().encode(text)
23
- let binary = ''
24
- for (const b of bytes) binary += String.fromCharCode(b)
25
- const b64 = btoa(binary)
26
- // base64url: +/ -> -_,去掉末尾 padding =
27
- return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
28
- }
29
-
30
- /**
31
- * Mermaid 源码编码成可安全放入 attribute 的 base64url(UTF-8)
32
- * 给前端组件(非 markdown 渲染路径)复用,避免重复实现。
33
- */
34
- export function encodeMermaidCodeToBase64Url(code: string): string {
35
- return encodeBase64Utf8(code)
36
- }
37
-
38
- /**
39
- * base64url 解码(UTF-8)
40
- */
41
- function decodeBase64Utf8(b64: string): string {
42
- // 兼容:某些情况下 '+' 可能被变成空格
43
- const normalized = b64.replace(/\s/g, '+')
44
- // base64url -> base64
45
- let base64 = normalized.replace(/-/g, '+').replace(/_/g, '/')
46
- // 补齐 padding
47
- const pad = base64.length % 4
48
- if (pad === 2) base64 += '=='
49
- else if (pad === 3) base64 += '='
50
- else if (pad !== 0) {
51
- // 非法长度,交给 atob 抛错
52
- }
53
-
54
- const binary = atob(base64)
55
- const bytes = new Uint8Array(binary.length)
56
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
57
- return new TextDecoder().decode(bytes)
58
- }
59
-
60
- /**
61
- * 初始化 Mermaid(仅需调用一次)
62
- */
63
- export function initMermaid(): void {
64
- if (mermaidInitialized) return
65
-
66
- mermaid.initialize({
67
- startOnLoad: false,
68
- theme: 'dark',
69
- securityLevel: 'loose',
70
- fontFamily: 'inherit',
71
- flowchart: {
72
- useMaxWidth: true,
73
- htmlLabels: true,
74
- curve: 'basis',
75
- },
76
- sequence: {
77
- useMaxWidth: true,
78
- },
79
- gantt: {
80
- useMaxWidth: true,
81
- },
82
- })
83
-
84
- mermaidInitialized = true
85
- }
86
-
87
- /**
88
- * 渲染页面中的 Mermaid 图表
89
- * 用户点击"显示图表"按钮后调用
90
- * @param container 容器元素(可选,默认为 document)
91
- */
92
- export async function renderMermaidDiagrams(container?: Element): Promise<void> {
93
- if (!mermaidInitialized) {
94
- initMermaid()
95
- }
96
-
97
- const root = container || document
98
- // 只选择未渲染的占位符
99
- const elements = root.querySelectorAll('.mermaid-placeholder:not(.mermaid-rendered):not(.mermaid-error-container)')
100
-
101
- for (const el of elements) {
102
- const b64 = el.getAttribute('data-mermaid-code-b64')
103
- if (!b64) continue
104
-
105
- let code = ''
106
- try {
107
- code = decodeBase64Utf8(b64)
108
- } catch (e) {
109
- console.warn('Mermaid 解码失败:', e)
110
- el.innerHTML = `<div class="mermaid-error">图表解码失败</div>`
111
- el.classList.remove('mermaid-placeholder')
112
- el.classList.add('mermaid-error-container')
113
- continue
114
- }
115
-
116
- // 显示加载中
117
- el.innerHTML = `<div class="mermaid-loading">渲染中...</div>`
118
-
119
- try {
120
- const id = `mermaid-${Math.random().toString(36).slice(2, 11)}`
121
- const { svg } = await mermaid.render(id, code)
122
- el.innerHTML = svg
123
- el.classList.remove('mermaid-placeholder')
124
- el.classList.add('mermaid-rendered')
125
- el.removeAttribute('data-mermaid-code-b64')
126
- } catch (error) {
127
- console.warn('Mermaid 渲染失败:', error)
128
- // 直接显示错误和源码
129
- el.innerHTML = `<div class="mermaid-error">图表渲染失败</div><pre class="mermaid-source">${code.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</pre>`
130
- el.classList.remove('mermaid-placeholder')
131
- el.classList.add('mermaid-error-container')
132
- }
133
- }
134
- }
135
-
136
- /**
137
- * 渲染 LaTeX 公式
138
- * @param latex LaTeX 字符串
139
- * @param displayMode 是否为块级公式
140
- * @returns 渲染后的 HTML
141
- */
142
- function renderLatex(latex: string, displayMode: boolean): string {
143
- try {
144
- return katex.renderToString(latex, {
145
- displayMode,
146
- throwOnError: false,
147
- strict: false,
148
- trust: true,
149
- output: 'html',
150
- })
151
- } catch (error) {
152
- // 渲染失败时返回原文
153
- console.warn('LaTeX 渲染失败:', error)
154
- return displayMode
155
- ? `<div class="latex-error">$$${latex}$$</div>`
156
- : `<span class="latex-error">$${latex}$</span>`
157
- }
158
- }
159
-
160
- /**
161
- * 渲染 fenced 的 LaTeX 代码块(```latex / ```katex),用于“先代码、后渲染”的视图切换。
162
- * 注意:这里只返回 HTML 字符串,容器样式由 `.latex-block` 控制。
163
- */
164
- export function renderLatexBlockToHtml(code: string): string {
165
- // 滚动条样式与系统对齐:直接复用全局 `.chat-scrollbar`
166
- return `<div class="latex-block chat-scrollbar">${renderLatex(code.trim(), true)}</div>`
167
- }
168
-
169
- /**
170
- * 预处理文本中的 LaTeX 公式
171
- * 将 LaTeX 公式替换为占位符,避免被 markdown 解析器处理
172
- * @param text 原始文本
173
- * @returns { processed: 处理后文本, placeholders: 占位符映射 }
174
- */
175
- function preprocessLatex(text: string): { processed: string; placeholders: Map<string, string> } {
176
- const placeholders = new Map<string, string>()
177
- let counter = 0
178
-
179
- // 生成唯一占位符(使用 HTML 注释格式,避免被 markdown 解析)
180
- const createPlaceholder = () => {
181
- const placeholder = `<!--LATEX:${counter++}-->`
182
- return placeholder
183
- }
184
-
185
- let processed = text
186
-
187
- // 处理块级公式 $$...$$ (优先处理,避免和行内公式冲突)
188
- processed = processed.replace(/\$\$([\s\S]*?)\$\$/g, (_, latex) => {
189
- const placeholder = createPlaceholder()
190
- const rendered = renderLatex(latex.trim(), true)
191
- placeholders.set(placeholder, `<div class="latex-block chat-scrollbar">${rendered}</div>`)
192
- return placeholder
193
- })
194
-
195
- // 处理块级公式 \[...\]
196
- processed = processed.replace(/\\\[([\s\S]*?)\\\]/g, (_, latex) => {
197
- const placeholder = createPlaceholder()
198
- const rendered = renderLatex(latex.trim(), true)
199
- placeholders.set(placeholder, `<div class="latex-block chat-scrollbar">${rendered}</div>`)
200
- return placeholder
201
- })
202
-
203
- // 处理行内公式 $...$ (单个 $,不能跨行)
204
- // 注意:避免匹配 $$ 和转义的 \$
205
- processed = processed.replace(/(?<!\$)\$(?!\$)((?:\\.|[^$\n])+?)\$(?!\$)/g, (_, latex) => {
206
- const placeholder = createPlaceholder()
207
- const rendered = renderLatex(latex.trim(), false)
208
- placeholders.set(placeholder, `<span class="latex-inline">${rendered}</span>`)
209
- return placeholder
210
- })
211
-
212
- // 处理行内公式 \(...\)
213
- processed = processed.replace(/\\\(([\s\S]*?)\\\)/g, (_, latex) => {
214
- const placeholder = createPlaceholder()
215
- const rendered = renderLatex(latex.trim(), false)
216
- placeholders.set(placeholder, `<span class="latex-inline">${rendered}</span>`)
217
- return placeholder
218
- })
219
-
220
- return { processed, placeholders }
221
- }
222
-
223
- /**
224
- * 后处理:将占位符替换回渲染后的 LaTeX
225
- */
226
- function postprocessLatex(html: string, placeholders: Map<string, string>): string {
227
- let result = html
228
- placeholders.forEach((rendered, placeholder) => {
229
- result = result.replace(placeholder, rendered)
230
- })
231
- return result
232
- }
233
-
234
- /**
235
- * 渲染 Markdown 为 HTML
236
- * @param markdown Markdown 文本
237
- * @returns 安全的 HTML 字符串
238
- */
239
- export function renderMarkdown(markdown: string): string {
240
- if (!markdown) return ''
241
-
242
- // 预处理 LaTeX 公式
243
- const { processed, placeholders } = preprocessLatex(markdown)
244
-
245
- // 创建自定义渲染器(继承默认渲染器)
246
- const renderer = new Renderer()
247
-
248
- // 自定义代码块渲染(支持 Mermaid)
249
- renderer.code = (code: string, language?: string) => {
250
- const lang = (language || 'plaintext').toLowerCase()
251
-
252
- // Mermaid 图表:生成占位符,延迟渲染
253
- if (lang === 'mermaid') {
254
- // 注意:HTML attribute 会把换行规范化成空格,直接写入会破坏 mermaid 语法
255
- // 所以这里用 base64(UTF-8) 存储源码,渲染时再解码
256
- const b64 = encodeBase64Utf8(code)
257
- return `<div class="mermaid-placeholder" data-mermaid-code-b64="${b64}"><div class="mermaid-loading">加载图表中...</div></div>`
258
- }
259
-
260
- const highlighted = highlightCode(code, language)
261
- // 滚动条样式与系统对齐:直接复用全局 `.chat-scrollbar`
262
- return `<pre class="markdown-code-block chat-scrollbar"><code class="language-${lang}">${highlighted}</code></pre>`
263
- }
264
-
265
- // 自定义表格渲染(添加样式类和滚动容器)
266
- renderer.table = (header: string, body: string) => {
267
- // 滚动条样式与系统对齐:直接复用全局 `.chat-scrollbar`
268
- return `<div class="markdown-table-wrapper chat-scrollbar"><table class="markdown-table"><thead>${header}</thead><tbody>${body}</tbody></table></div>`
269
- }
270
-
271
- // 自定义链接渲染(添加 target="_blank" 和 rel="noopener noreferrer")
272
- renderer.link = (href: string, title: string | null, text: string) => {
273
- const titleAttr = title ? ` title="${title}"` : ''
274
- return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer" class="markdown-link">${text}</a>`
275
- }
276
-
277
- // 自定义列表项渲染,将 checkbox 替换为文本 [x] 或 [ ]
278
- renderer.listitem = (text: string, task: boolean, checked: boolean) => {
279
- if (task) {
280
- // 任务列表:将 checkbox 替换为文本
281
- const checkboxText = checked ? '[x]' : '[ ]'
282
- // 移除 marked 生成的 checkbox input,替换为文本
283
- const textWithoutCheckbox = text.replace(/<input[^>]*>/g, '').trim()
284
- return `<li class="markdown-task-item"><span class="markdown-task-checkbox">${checkboxText}</span> ${textWithoutCheckbox}</li>`
285
- }
286
- // 普通列表项
287
- return `<li>${text}</li>`
288
- }
289
-
290
- // 配置 marked(marked 12.x 使用新的配置方式)
291
- marked.setOptions({
292
- breaks: true, // 支持换行
293
- gfm: true, // GitHub Flavored Markdown
294
- })
295
-
296
- // 渲染 Markdown(marked 12.x 返回字符串,不是 Promise)
297
- let html = marked(processed, { renderer }) as string
298
-
299
- // 后处理:将占位符替换回渲染后的 LaTeX
300
- html = postprocessLatex(html, placeholders)
301
-
302
- // 使用 DOMPurify 清理 HTML,防止 XSS
303
- // 允许 KaTeX 生成的 SVG 和相关元素
304
- html = DOMPurify.sanitize(html, {
305
- ALLOWED_TAGS: [
306
- 'p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre',
307
- 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
308
- 'ul', 'ol', 'li', 'blockquote',
309
- 'table', 'thead', 'tbody', 'tr', 'th', 'td',
310
- 'a', 'img', 'hr',
311
- 'div', 'span',
312
- // KaTeX 相关标签
313
- 'math', 'semantics', 'mrow', 'mi', 'mn', 'mo', 'msup', 'msub',
314
- 'mfrac', 'mroot', 'msqrt', 'mtext', 'mspace', 'mtable', 'mtr', 'mtd',
315
- 'annotation', 'mover', 'munder', 'munderover', 'menclose', 'mpadded',
316
- 'svg', 'line', 'path', 'rect', 'circle', 'g', 'use', 'defs', 'symbol',
317
- ],
318
- ALLOWED_ATTR: [
319
- 'href', 'title', 'target', 'rel', 'class',
320
- 'src', 'alt', 'width', 'height',
321
- // Mermaid 相关属性
322
- 'data-mermaid-code-b64',
323
- // KaTeX 相关属性
324
- 'style', 'xmlns', 'viewBox', 'preserveAspectRatio',
325
- 'd', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'r', 'cx', 'cy',
326
- 'fill', 'stroke', 'stroke-width', 'transform',
327
- 'xlink:href', 'aria-hidden', 'focusable', 'role',
328
- 'mathvariant', 'encoding', 'stretchy', 'fence', 'separator',
329
- 'lspace', 'rspace', 'minsize', 'maxsize', 'accent', 'accentunder',
330
- ],
331
- ADD_ATTR: ['xmlns:xlink'],
332
- })
333
-
334
- return html
335
- }