@templmf/temp-solf-lmf 0.0.55 → 0.0.57
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/fe-flow.md +348 -0
- package/package.json +1 -1
- package/ui-parse.md +308 -0
- package//345/242/236/351/207/217/351/234/200/346/261/202prompt +72 -0
- package/guanwang/README.md +0 -95
- package/guanwang/docs/changelog.md +0 -145
- package/guanwang/docs/doc-maintenance.md +0 -229
- package/guanwang/docs/product.md +0 -181
- package/guanwang/docs/test-cases.md +0 -395
- package/guanwang/docs/usage.md +0 -291
- package/guanwang/env.example +0 -27
- package/guanwang/index.html +0 -13
- package/guanwang/package-lock.json +0 -3825
- package/guanwang/package.json +0 -32
- package/guanwang/public/favicon.svg +0 -4
- package/guanwang/public/react-runtime/babel.min.js +0 -4
- package/guanwang/public/react-runtime/react-dom.min.js +0 -267
- package/guanwang/public/react-runtime/react.min.js +0 -31
- package/guanwang/public/vue-repl-assets/compiler-sfc.esm-browser.js +0 -50795
- package/guanwang/public/vue-repl-assets/runtime-dom.esm-browser.js +0 -12758
- package/guanwang/public/vue-repl-assets/server-renderer.esm-browser.js +0 -8600
- package/guanwang/public/vue-repl-assets/vue.esm-browser.js +0 -18672
- package/guanwang/src/App.vue +0 -61
- package/guanwang/src/chat-sdk/core/components/ChatBox.vue +0 -305
- package/guanwang/src/chat-sdk/core/components/ChatSidebar.vue +0 -84
- package/guanwang/src/chat-sdk/core/components/InputBar.vue +0 -354
- package/guanwang/src/chat-sdk/core/components/MessageBubble.vue +0 -703
- package/guanwang/src/chat-sdk/core/useTheme.js +0 -31
- package/guanwang/src/chat-sdk/features/artifact/ArtifactCard.vue +0 -172
- package/guanwang/src/chat-sdk/features/artifact/ArtifactPanel.vue +0 -963
- package/guanwang/src/chat-sdk/features/artifact/index.js +0 -13
- package/guanwang/src/chat-sdk/features/artifact/useArtifactStore.js +0 -275
- package/guanwang/src/chat-sdk/features/codepreview/CodePreview.vue +0 -523
- package/guanwang/src/chat-sdk/features/codepreview/index.js +0 -7
- package/guanwang/src/chat-sdk/features/markdown/index.js +0 -13
- package/guanwang/src/chat-sdk/features/markdown/useMarkdown.js +0 -724
- package/guanwang/src/chat-sdk/features/mermaid/MermaidZoom.vue +0 -254
- package/guanwang/src/chat-sdk/features/upload/FileAttachment.vue +0 -142
- package/guanwang/src/chat-sdk/features/upload/index.js +0 -17
- package/guanwang/src/chat-sdk/features/upload/useFileHandler.js +0 -336
- package/guanwang/src/chat-sdk/headless/api/adapters/openai.js +0 -76
- package/guanwang/src/chat-sdk/headless/api/chatApi.js +0 -126
- package/guanwang/src/chat-sdk/headless/buildSystemPrompt.js +0 -351
- package/guanwang/src/chat-sdk/headless/index.js +0 -15
- package/guanwang/src/chat-sdk/headless/useChat.js +0 -77
- package/guanwang/src/chat-sdk/headless/useChatDB.js +0 -147
- package/guanwang/src/chat-sdk/headless/useChatStore.js +0 -529
- package/guanwang/src/chat-sdk/index.js +0 -79
- package/guanwang/src/chat-sdk/modes/architect.js +0 -27
- package/guanwang/src/chat-sdk/modes/ask.js +0 -26
- package/guanwang/src/chat-sdk/modes/code.js +0 -25
- package/guanwang/src/chat-sdk/modes/index.js +0 -36
- package/guanwang/src/chat-sdk/modes/requirements.js +0 -175
- package/guanwang/src/chat-sdk/settings/SettingsPanel.vue +0 -170
- package/guanwang/src/chat-sdk/settings/index.js +0 -9
- package/guanwang/src/chat-sdk/settings/useSettings.js +0 -122
- package/guanwang/src/chat-sdk/tools/defaults.js +0 -89
- package/guanwang/src/chat-sdk/tools/index.js +0 -16
- package/guanwang/src/chat-sdk/tools/parser.js +0 -116
- package/guanwang/src/components/CustomCursor.vue +0 -69
- package/guanwang/src/components/Footer.vue +0 -24
- package/guanwang/src/components/LoginModal.vue +0 -109
- package/guanwang/src/components/Navbar.vue +0 -193
- package/guanwang/src/components/ThemeToggle.vue +0 -25
- package/guanwang/src/composables/useArtifactStore.js +0 -253
- package/guanwang/src/composables/useAuth.js +0 -88
- package/guanwang/src/composables/useChatDB.js +0 -147
- package/guanwang/src/composables/useCountUp.js +0 -24
- package/guanwang/src/composables/useFileHandler.js +0 -345
- package/guanwang/src/composables/useTheme.js +0 -31
- package/guanwang/src/config/api.js +0 -71
- package/guanwang/src/main.js +0 -23
- package/guanwang/src/router/index.js +0 -23
- package/guanwang/src/services/authApi.js +0 -27
- package/guanwang/src/services/chatApi.js +0 -66
- package/guanwang/src/styles/global.css +0 -478
- package/guanwang/src/tracker/analyze.js +0 -73
- package/guanwang/src/tracker/config.js +0 -82
- package/guanwang/src/tracker/index.js +0 -18
- package/guanwang/src/tracker/service.js +0 -102
- package/guanwang/src/tracker/useChatTracker.js +0 -179
- package/guanwang/src/tracker/useTracker.js +0 -45
- package/guanwang/src/views/ChatView.vue +0 -65
- package/guanwang/src/views/HomeView.vue +0 -156
- package/guanwang/src/views/MarketView.vue +0 -143
- package/guanwang/src/views/PracticesView.vue +0 -190
- package/guanwang/src/views/SkillsView.vue +0 -129
- package/guanwang/temp +0 -19
- package/guanwang/vite.config.js +0 -6
|
@@ -1,724 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useMarkdown.js v3
|
|
3
|
-
*
|
|
4
|
-
* 核心改动:废弃全局 _artifactStore 注入模式
|
|
5
|
-
* renderMarkdown / createStreamingParser 直接接收 renderContext 参数:
|
|
6
|
-
* { messageId, artifactStore }
|
|
7
|
-
* buildCodeBlock 通过函数参数拿到上下文,多实例完全隔离,无竞争。
|
|
8
|
-
*/
|
|
9
|
-
import { ref } from 'vue'
|
|
10
|
-
import { marked } from 'marked'
|
|
11
|
-
import DOMPurify from 'dompurify'
|
|
12
|
-
|
|
13
|
-
// ─────────────────────────────────────────────────────────────────
|
|
14
|
-
// Shiki 单例(全局共享,只初始化一次)
|
|
15
|
-
// ─────────────────────────────────────────────────────────────────
|
|
16
|
-
let _hl = null
|
|
17
|
-
let _hlLoading = false
|
|
18
|
-
export const shikiReady = ref(false)
|
|
19
|
-
|
|
20
|
-
const SUPPORTED_LANGS = [
|
|
21
|
-
'javascript','typescript','vue','html','css','scss',
|
|
22
|
-
'json','bash','shell','python','java','go','rust',
|
|
23
|
-
'markdown','yaml','toml','sql','graphql','xml','jsx','tsx',
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
// Shiki 初始化 Promise(单例,并发调用共享同一个 Promise,避免 setInterval 泄漏)
|
|
27
|
-
let _hlPromise = null
|
|
28
|
-
|
|
29
|
-
async function getShiki() {
|
|
30
|
-
if (_hl) return _hl
|
|
31
|
-
if (_hlPromise) return _hlPromise // 并发调用等同一个 Promise
|
|
32
|
-
_hlPromise = (async () => {
|
|
33
|
-
try {
|
|
34
|
-
const { createHighlighter } = await import('shiki')
|
|
35
|
-
_hl = await createHighlighter({ themes: ['one-dark-pro','github-light'], langs: SUPPORTED_LANGS })
|
|
36
|
-
shikiReady.value = true
|
|
37
|
-
} catch (e) {
|
|
38
|
-
console.warn('[shiki] init failed', e)
|
|
39
|
-
} finally {
|
|
40
|
-
_hlPromise = null // 无论成功失败都重置,下次可重试
|
|
41
|
-
}
|
|
42
|
-
return _hl
|
|
43
|
-
})()
|
|
44
|
-
return _hlPromise
|
|
45
|
-
}
|
|
46
|
-
getShiki() // 预热
|
|
47
|
-
|
|
48
|
-
function currentTheme() {
|
|
49
|
-
return document.documentElement.getAttribute('data-theme') === 'light'
|
|
50
|
-
? 'github-light' : 'one-dark-pro'
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function highlightCode(code, lang) {
|
|
54
|
-
const plainHtml = `<pre class="shiki-pre"><code>${escapeHtml(code)}</code></pre>`
|
|
55
|
-
if (!_hl) return plainHtml
|
|
56
|
-
// 语言不在支持列表时直接走 plain fallback,避免 Shiki 内部用 'text' 语言输出空内容
|
|
57
|
-
if (!_hl.getLoadedLanguages().includes(lang)) return plainHtml
|
|
58
|
-
try { return _hl.codeToHtml(code, { lang, theme: currentTheme() }) }
|
|
59
|
-
catch { return plainHtml }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ─────────────────────────────────────────────────────────────────
|
|
63
|
-
// KaTeX
|
|
64
|
-
// ─────────────────────────────────────────────────────────────────
|
|
65
|
-
function renderKatex(tex, display) {
|
|
66
|
-
try { return window.katex?.renderToString(tex, { displayMode: display, throwOnError: false }) ?? tex }
|
|
67
|
-
catch { return tex }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ─────────────────────────────────────────────────────────────────
|
|
71
|
-
// 工具
|
|
72
|
-
// ─────────────────────────────────────────────────────────────────
|
|
73
|
-
function escapeHtml(s) {
|
|
74
|
-
return (s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
75
|
-
.replace(/"/g,'"').replace(/'/g,''')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let _uid = 0
|
|
79
|
-
function uid() { return `mb${++_uid}` }
|
|
80
|
-
|
|
81
|
-
// 全局代码块内容注册表(供 handleCodeAction 复制/预览用)
|
|
82
|
-
// 用 messageId 分组,组件卸载时调用 clearBlocksByMessage 清理,防止无限堆积
|
|
83
|
-
export const pendingBlocks = new Map()
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 清理指定 messageId 下的所有代码块缓存
|
|
87
|
-
* 由 MessageBubble onUnmounted 调用
|
|
88
|
-
*/
|
|
89
|
-
export function clearBlocksByMessage(messageId) {
|
|
90
|
-
if (!messageId) return
|
|
91
|
-
for (const [key, val] of pendingBlocks) {
|
|
92
|
-
if (val.messageId === messageId) pendingBlocks.delete(key)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// 流式 artifact Loading 卡片里显示的语言标签(简短版)
|
|
97
|
-
const LANG_LABEL_INLINE = {
|
|
98
|
-
vue:'Vue SFC', html:'HTML', jsx:'React JSX', tsx:'React TSX',
|
|
99
|
-
javascript:'JavaScript', js:'JavaScript', typescript:'TypeScript', ts:'TypeScript',
|
|
100
|
-
python:'Python', go:'Go', rust:'Rust', java:'Java', kotlin:'Kotlin', swift:'Swift',
|
|
101
|
-
css:'CSS', scss:'SCSS', bash:'Shell', sh:'Shell',
|
|
102
|
-
json:'JSON', yaml:'YAML', toml:'TOML', sql:'SQL',
|
|
103
|
-
mermaid:'Mermaid', svg:'SVG', math:'Math', latex:'LaTeX', markdown:'Markdown',
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ─────────────────────────────────────────────────────────────────
|
|
107
|
-
// SVG 图标常量
|
|
108
|
-
// ─────────────────────────────────────────────────────────────────
|
|
109
|
-
const SVG_COPY = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`
|
|
110
|
-
const SVG_EYE = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>`
|
|
111
|
-
const SVG_ZOOM = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>`
|
|
112
|
-
const PREVIEWABLE = new Set(['html','vue','jsx','tsx','javascript','js','css','svg','markdown','md'])
|
|
113
|
-
|
|
114
|
-
// ─────────────────────────────────────────────────────────────────
|
|
115
|
-
// buildCodeBlock:接收渲染上下文,完全隔离
|
|
116
|
-
// ctx = { messageId, artifactStore, blockIndex }
|
|
117
|
-
// ─────────────────────────────────────────────────────────────────
|
|
118
|
-
function buildCodeBlock(lang, code, id, isStreaming = false, ctx = null) {
|
|
119
|
-
const displayLang = lang || 'text'
|
|
120
|
-
|
|
121
|
-
// 改动4:code 为空时不渲染任何代码块
|
|
122
|
-
if (!code || !code.trim()) return ''
|
|
123
|
-
|
|
124
|
-
pendingBlocks.set(id, { lang: displayLang, code, messageId: ctx?.messageId })
|
|
125
|
-
|
|
126
|
-
// ── Artifact 路由 ──────────────────────────────────────────────
|
|
127
|
-
const store = ctx?.artifactStore
|
|
128
|
-
const msgId = ctx?.messageId
|
|
129
|
-
if (store && msgId) {
|
|
130
|
-
const forceArtifact = ctx?.isArtifact // AI 显式标记了 [artifact]
|
|
131
|
-
const autoArtifact = store.shouldBeArtifact?.(lang, code) // 兜底规则
|
|
132
|
-
if (forceArtifact || autoArtifact) {
|
|
133
|
-
const artId = store.register({ messageId: msgId, lang: displayLang, code, index: ctx.blockIndex || 0 })
|
|
134
|
-
pendingBlocks.set(id, { lang: displayLang, code, artId, messageId: ctx?.messageId })
|
|
135
|
-
return `<div class="artifact-placeholder" data-art-id="${artId}" data-block-id="${id}"></div>`
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ── 特殊块类型 ─────────────────────────────────────────────────
|
|
140
|
-
if (lang === 'mermaid') {
|
|
141
|
-
return `<div class="code-wrapper mermaid-wrapper" id="${id}">
|
|
142
|
-
<div class="cb-actions"><span class="cb-lang">mermaid</span>
|
|
143
|
-
<div style="display:flex;gap:4px"><button class="cb-btn" data-action="zoom-mermaid" data-id="${id}">${SVG_ZOOM} 放大</button><button class="cb-btn" data-action="copy" data-id="${id}">${SVG_COPY} 复制</button></div>
|
|
144
|
-
</div>
|
|
145
|
-
<div class="mermaid-container">
|
|
146
|
-
<div class="mermaid-chart mermaid-pending" data-mermaid="${encodeURIComponent(code)}">
|
|
147
|
-
<div class="mermaid-placeholder"><span class="mermaid-placeholder__dot"/><span class="mermaid-placeholder__dot"/><span class="mermaid-placeholder__dot"/></div>
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
</div>`
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (lang === 'math' || lang === 'latex') {
|
|
154
|
-
return `<div class="code-wrapper math-wrapper" id="${id}">
|
|
155
|
-
<div class="cb-actions"><span class="cb-lang">math</span></div>
|
|
156
|
-
<div class="math-display">${renderKatex(code, true)}</div>
|
|
157
|
-
</div>`
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ── 普通代码块 ─────────────────────────────────────────────────
|
|
161
|
-
const previewBtn = PREVIEWABLE.has(lang) && !ctx?.noPreview
|
|
162
|
-
? `<button class="cb-btn" data-action="preview" data-id="${id}">${SVG_EYE} 预览</button>` : ''
|
|
163
|
-
const encodedCode = btoa(unescape(encodeURIComponent(code)))
|
|
164
|
-
const shikiHtml = `<div class="shiki-live" data-id="${id}" data-lang="${lang}" data-code="${encodedCode}">${highlightCode(code, lang)}</div>`
|
|
165
|
-
|
|
166
|
-
return `<div class="code-wrapper${isStreaming ? ' code-streaming' : ''}" id="${id}">
|
|
167
|
-
<div class="cb-actions">
|
|
168
|
-
<span class="cb-lang">${displayLang}</span>
|
|
169
|
-
<div style="display:flex;gap:4px">${previewBtn}
|
|
170
|
-
<button class="cb-btn" data-action="copy" data-id="${id}">${SVG_COPY} 复制</button>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
${shikiHtml}
|
|
174
|
-
</div>`
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ─────────────────────────────────────────────────────────────────
|
|
178
|
-
// makeRenderer:每次渲染时创建,携带当次的上下文
|
|
179
|
-
// ─────────────────────────────────────────────────────────────────
|
|
180
|
-
function makeRenderer(ctx = null) {
|
|
181
|
-
const r = new marked.Renderer()
|
|
182
|
-
let blockIndex = 0
|
|
183
|
-
r.code = ({ text, lang }) => {
|
|
184
|
-
blockIndex++
|
|
185
|
-
return buildCodeBlock(lang || 'text', text || '', uid(), false, ctx ? { ...ctx, blockIndex } : null)
|
|
186
|
-
}
|
|
187
|
-
r.codespan = ({ text }) => `<code class="inline-code">${escapeHtml(text)}</code>`
|
|
188
|
-
r.image = (href, title, text) =>
|
|
189
|
-
`<img src="${href||''}" alt="${escapeHtml(text||'')}" title="${escapeHtml(title||'')}" loading="lazy" style="max-width:100%;border-radius:8px;margin:8px 0"/>`
|
|
190
|
-
r.link = (href, title, text) =>
|
|
191
|
-
`<a href="${href||''}" target="_blank" rel="noopener noreferrer">${text||href||''}</a>`
|
|
192
|
-
return r
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ─────────────────────────────────────────────────────────────────
|
|
196
|
-
// LaTeX 预处理
|
|
197
|
-
// ─────────────────────────────────────────────────────────────────
|
|
198
|
-
function preprocessLatex(md) {
|
|
199
|
-
md = md.replace(/\$\$([^$]+?)\$\$/gs, (_, t) => `\n\`\`\`math\n${t.trim()}\n\`\`\`\n`)
|
|
200
|
-
md = md.replace(/(?<!\$)\$(?!\$)([^$\n]+?)\$(?!\$)/g, (_, t) => {
|
|
201
|
-
try { return window.katex?.renderToString(t, { displayMode: false, throwOnError: false }) ?? `$${t}$` }
|
|
202
|
-
catch { return `$${t}$` }
|
|
203
|
-
})
|
|
204
|
-
return md
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// ─────────────────────────────────────────────────────────────────
|
|
208
|
-
// DOMPurify 配置
|
|
209
|
-
// ─────────────────────────────────────────────────────────────────
|
|
210
|
-
const PURIFY_OPTS = {
|
|
211
|
-
ADD_TAGS: ['math','semantics','mrow','mi','mo','mn','msup','msub','mfrac','annotation'],
|
|
212
|
-
ADD_ATTR: ['data-action','data-id','data-lang','data-mermaid','data-code','data-art-id','data-block-id','xmlns','encoding'],
|
|
213
|
-
FORCE_BODY: true,
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ─────────────────────────────────────────────────────────────────
|
|
217
|
-
// StreamingMarkdownParser
|
|
218
|
-
// ─────────────────────────────────────────────────────────────────
|
|
219
|
-
class StreamingMarkdownParser {
|
|
220
|
-
constructor(ctx = null) {
|
|
221
|
-
this.ctx = ctx // { messageId, artifactStore }
|
|
222
|
-
this.reset()
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
reset() {
|
|
226
|
-
this.inCode = false
|
|
227
|
-
this.codeLang = ''
|
|
228
|
-
this.isArtifact = false
|
|
229
|
-
this.codeLines = []
|
|
230
|
-
this.codeBlockId = null
|
|
231
|
-
this.fenceChar = '`'
|
|
232
|
-
this.fenceLen = 3
|
|
233
|
-
this.innerDepth = 0 // 内层未关闭围栏计数
|
|
234
|
-
this._streamArtIdx = 0
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
parse(content) {
|
|
238
|
-
const lines = content.split('\n')
|
|
239
|
-
let i = 0
|
|
240
|
-
const closedMdChunks = []
|
|
241
|
-
let streamingCodeLang = null
|
|
242
|
-
let streamingCodeLines = null
|
|
243
|
-
let tailText = ''
|
|
244
|
-
|
|
245
|
-
while (i < lines.length) {
|
|
246
|
-
const line = lines[i]
|
|
247
|
-
if (!this.inCode) {
|
|
248
|
-
const fence = line.match(/^(`{3,}|~{3,})(.*)/)
|
|
249
|
-
if (fence) {
|
|
250
|
-
if (tailText) { closedMdChunks.push(tailText); tailText = '' }
|
|
251
|
-
const infoLine = fence[2].trim()
|
|
252
|
-
const parts = infoLine.split(/\s+/)
|
|
253
|
-
this.inCode = true
|
|
254
|
-
this.fenceChar = fence[1][0]
|
|
255
|
-
this.fenceLen = fence[1].length
|
|
256
|
-
this.codeLang = parts[0] || 'text'
|
|
257
|
-
this.isArtifact = parts.includes('[artifact]')
|
|
258
|
-
this.codeLines = []
|
|
259
|
-
this.innerDepth = 0
|
|
260
|
-
i++; continue
|
|
261
|
-
}
|
|
262
|
-
tailText += (tailText ? '\n' : '') + line
|
|
263
|
-
} else {
|
|
264
|
-
// 精确匹配外层关闭围栏 + innerDepth 追踪内层代码块
|
|
265
|
-
// 防止 markdown 等语言内部的嵌套围栏行被误判为外层关闭
|
|
266
|
-
const closeRe = new RegExp(`^${this.fenceChar}{${this.fenceLen}}\\s*$`)
|
|
267
|
-
const innerOpenM = line.match(new RegExp(`^${this.fenceChar}{3,}(\\S+)`))
|
|
268
|
-
if (closeRe.test(line)) {
|
|
269
|
-
if (this.innerDepth > 0) {
|
|
270
|
-
// 内层代码块的关闭行,不是外层关闭,继续收集
|
|
271
|
-
this.innerDepth--
|
|
272
|
-
this.codeLines.push(line)
|
|
273
|
-
} else {
|
|
274
|
-
// 真正的外层关闭
|
|
275
|
-
const info = this.isArtifact ? `${this.codeLang} [artifact]` : this.codeLang
|
|
276
|
-
closedMdChunks.push(`\`\`\`${info}\n${this.codeLines.join('\n')}\n\`\`\``)
|
|
277
|
-
this.inCode = false; this.codeLang = ''; this.isArtifact = false
|
|
278
|
-
this.codeLines = []; this.fenceLen = 3; this.fenceChar = '`'
|
|
279
|
-
this.innerDepth = 0
|
|
280
|
-
}
|
|
281
|
-
i++; continue
|
|
282
|
-
} else if (innerOpenM) {
|
|
283
|
-
// 内层代码块开启(带语言标识符),计数加一
|
|
284
|
-
this.innerDepth++
|
|
285
|
-
}
|
|
286
|
-
this.codeLines.push(line)
|
|
287
|
-
}
|
|
288
|
-
i++
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (this.inCode) {
|
|
292
|
-
streamingCodeLang = this.codeLang
|
|
293
|
-
streamingCodeLines = [...this.codeLines]
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// 渲染已闭合部分:使用 safeMarkdownParse,彻底避免 marked 被嵌套围栏截断
|
|
297
|
-
let closedHtml = ''
|
|
298
|
-
if (closedMdChunks.length > 0) {
|
|
299
|
-
closedHtml = safeMarkdownParse(closedMdChunks.join('\n\n'), this.ctx)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// 流式尾部
|
|
303
|
-
let tailHtml = ''
|
|
304
|
-
if (streamingCodeLang !== null) {
|
|
305
|
-
const code = streamingCodeLines.join('\n')
|
|
306
|
-
if (!this.codeBlockId) this.codeBlockId = uid()
|
|
307
|
-
const id = this.codeBlockId
|
|
308
|
-
pendingBlocks.set(id, { lang: streamingCodeLang, code, messageId: this.ctx?.messageId })
|
|
309
|
-
|
|
310
|
-
const store = this.ctx?.artifactStore
|
|
311
|
-
const msgId = this.ctx?.messageId
|
|
312
|
-
// isArtifact:AI 在 infoLine 里显式声明了 [artifact],或由 shouldBeArtifact 兜底判断
|
|
313
|
-
const isArtifact = this.isArtifact || store?.shouldBeArtifact?.(streamingCodeLang, code)
|
|
314
|
-
|
|
315
|
-
if (store && msgId && isArtifact) {
|
|
316
|
-
if (!this._streamArtIdx) this._streamArtIdx = 1
|
|
317
|
-
const artId = store.preRegister?.({
|
|
318
|
-
messageId: msgId,
|
|
319
|
-
lang: streamingCodeLang,
|
|
320
|
-
index: this._streamArtIdx,
|
|
321
|
-
}) || store.register({
|
|
322
|
-
messageId: msgId,
|
|
323
|
-
lang: streamingCodeLang,
|
|
324
|
-
code,
|
|
325
|
-
index: this._streamArtIdx,
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
if (code) {
|
|
329
|
-
store.register({ messageId: msgId, lang: streamingCodeLang, code, index: this._streamArtIdx })
|
|
330
|
-
store.updateCode?.(artId, code)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const langLabel = LANG_LABEL_INLINE[streamingCodeLang] || streamingCodeLang
|
|
334
|
-
const lineCount = code ? code.split('\n').length : 0
|
|
335
|
-
const lineTip = lineCount > 0 ? `· ${lineCount} 行` : ''
|
|
336
|
-
tailHtml = `<div class="artifact-gen-hint">
|
|
337
|
-
<span class="artifact-gen-hint__dot"></span>
|
|
338
|
-
<span class="artifact-gen-hint__text">正在生成 ${langLabel} 文件 ${lineTip}</span>
|
|
339
|
-
</div>`
|
|
340
|
-
} else if (!code.trim()) {
|
|
341
|
-
tailHtml = ''
|
|
342
|
-
} else {
|
|
343
|
-
tailHtml = `<div class="code-wrapper code-streaming" id="${id}">
|
|
344
|
-
<div class="cb-actions">
|
|
345
|
-
<span class="cb-lang">${streamingCodeLang}</span>
|
|
346
|
-
<div class="cb-streaming-badge"><span class="cb-dot"></span>输出中</div>
|
|
347
|
-
</div>
|
|
348
|
-
<div class="shiki-live" data-id="${id}" data-lang="${streamingCodeLang}">${highlightCode(code, streamingCodeLang)}</div>
|
|
349
|
-
</div>`
|
|
350
|
-
}
|
|
351
|
-
} else if (tailText.trim()) {
|
|
352
|
-
tailHtml = renderInlineSafe(tailText)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return { closedHtml, tailHtml }
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function renderInlineSafe(text) {
|
|
360
|
-
// 先提取行内反引号内容,再 escape,最后还原,避免路径字符串被误处理
|
|
361
|
-
const inlines = []
|
|
362
|
-
const protected_ = text.replace(/`([^`\n]+?)`/g, (_, t) => {
|
|
363
|
-
inlines.push(t)
|
|
364
|
-
return `\x00IL${inlines.length - 1}\x00`
|
|
365
|
-
})
|
|
366
|
-
let html = escapeHtml(protected_)
|
|
367
|
-
html = html.replace(/\x00IL(\d+)\x00/g, (_, i) =>
|
|
368
|
-
`<code class="inline-code">${escapeHtml(inlines[+i])}</code>`
|
|
369
|
-
)
|
|
370
|
-
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
371
|
-
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
372
|
-
html = html.replace(/\n/g, '<br>')
|
|
373
|
-
return `<p class="streaming-tail">${html}</p>`
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// ─────────────────────────────────────────────────────────────────
|
|
377
|
-
// safeMarkdownParse
|
|
378
|
-
// 在交给 marked 之前,先把所有代码围栏块提取为 placeholder,
|
|
379
|
-
// 渲染后再把 placeholder 替换为 buildCodeBlock 的输出。
|
|
380
|
-
// 这样 marked 永远看不到嵌套围栏,彻底解决内容截断问题。
|
|
381
|
-
// ─────────────────────────────────────────────────────────────────
|
|
382
|
-
function safeMarkdownParse(content, ctx) {
|
|
383
|
-
const blocks = []
|
|
384
|
-
|
|
385
|
-
// ── 用逐行状态机提取代码围栏块 ──
|
|
386
|
-
// CommonMark:inCode 状态下所有行都是代码内容,只认精确匹配的外层关闭行,不做嵌套计数
|
|
387
|
-
const lines = content.split('\n')
|
|
388
|
-
let inCode = false
|
|
389
|
-
let fenceChar = '`'
|
|
390
|
-
let fenceLen = 3
|
|
391
|
-
let codeLang = ''
|
|
392
|
-
let isArtifact = false
|
|
393
|
-
let codeLines = []
|
|
394
|
-
let innerDepth = 0 // 内层未关闭围栏计数
|
|
395
|
-
const outLines = []
|
|
396
|
-
|
|
397
|
-
for (let i = 0; i < lines.length; i++) {
|
|
398
|
-
const line = lines[i]
|
|
399
|
-
if (!inCode) {
|
|
400
|
-
const m = line.match(/^(`{3,}|~{3,})(.*)/)
|
|
401
|
-
if (m) {
|
|
402
|
-
const infoLine = m[2].trim()
|
|
403
|
-
const parts = infoLine.split(/\s+/)
|
|
404
|
-
inCode = true
|
|
405
|
-
fenceChar = m[1][0]
|
|
406
|
-
fenceLen = m[1].length
|
|
407
|
-
codeLang = parts[0] || 'text'
|
|
408
|
-
isArtifact = parts.includes('[artifact]')
|
|
409
|
-
codeLines = []
|
|
410
|
-
innerDepth = 0
|
|
411
|
-
} else {
|
|
412
|
-
outLines.push(line)
|
|
413
|
-
}
|
|
414
|
-
} else {
|
|
415
|
-
// 精确匹配外层关闭围栏 + innerDepth 追踪内层代码块
|
|
416
|
-
// 防止 markdown 等语言内部的嵌套围栏行被误判为外层关闭
|
|
417
|
-
const closeRe = new RegExp(`^${fenceChar}{${fenceLen}}\\s*$`)
|
|
418
|
-
const innerOpenM = line.match(new RegExp(`^${fenceChar}{3,}(\\S+)`))
|
|
419
|
-
if (closeRe.test(line)) {
|
|
420
|
-
if (innerDepth > 0) {
|
|
421
|
-
// 内层代码块的关闭行,depth-- 后继续收集
|
|
422
|
-
innerDepth--
|
|
423
|
-
codeLines.push(line)
|
|
424
|
-
} else {
|
|
425
|
-
// 真正的外层关闭
|
|
426
|
-
const ph = `\x00CODEBLOCK_${blocks.length}\x00`
|
|
427
|
-
blocks.push({ lang: codeLang, code: codeLines.join('\n'), isArtifact })
|
|
428
|
-
outLines.push(ph)
|
|
429
|
-
inCode = false; codeLang = ''; isArtifact = false; codeLines = []; innerDepth = 0
|
|
430
|
-
}
|
|
431
|
-
} else {
|
|
432
|
-
if (innerOpenM) innerDepth++ // 内层代码块开启(带语言标识符),计数加一
|
|
433
|
-
codeLines.push(line)
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
// 未闭合的围栏(如流式截断):同样作为代码块处理,而不是丢成普通文本
|
|
438
|
-
// 这样 svg/vue/html 等语言仍能正确路由到面板,截断的代码也能显示代码块 UI
|
|
439
|
-
if (inCode && codeLines.length > 0) {
|
|
440
|
-
const ph = `\x00CODEBLOCK_${blocks.length}\x00`
|
|
441
|
-
blocks.push({ lang: codeLang, code: codeLines.join('\n'), isArtifact })
|
|
442
|
-
outLines.push(ph)
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const extracted = outLines.join('\n')
|
|
446
|
-
|
|
447
|
-
// ── 提前保护行内反引号,防止 gfm 模式下路径字符串被误解析 ──
|
|
448
|
-
const inlineCodes = []
|
|
449
|
-
const protectedContent = extracted.replace(/`([^`\n]+?)`/g, (_, text) => {
|
|
450
|
-
const ph = `\x00INLINE_${inlineCodes.length}\x00`
|
|
451
|
-
inlineCodes.push(text)
|
|
452
|
-
return ph
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
// 对非代码内容做 LaTeX 预处理,然后交给 marked
|
|
456
|
-
marked.use({ renderer: makeRenderer(null), gfm: true, breaks: true })
|
|
457
|
-
let html = marked.parse(preprocessLatex(protectedContent))
|
|
458
|
-
|
|
459
|
-
// 还原行内代码占位符
|
|
460
|
-
if (inlineCodes.length > 0) {
|
|
461
|
-
html = html.replace(/\x00INLINE_(\d+)\x00/g, (_, idx) =>
|
|
462
|
-
`<code class="inline-code">${escapeHtml(inlineCodes[+idx])}</code>`
|
|
463
|
-
)
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// 把 placeholder 替换为真实代码块 HTML(携带 ctx)
|
|
467
|
-
if (blocks.length > 0) {
|
|
468
|
-
let blockIndex = 0
|
|
469
|
-
// placeholder 可能被 marked 包在 <p> 里,先拆出来再替换
|
|
470
|
-
html = html.replace(/<p>(\x00CODEBLOCK_\d+\x00)<\/p>/g, '$1')
|
|
471
|
-
html = html.replace(/\x00CODEBLOCK_(\d+)\x00/g, (_, idx) => {
|
|
472
|
-
const { lang, code, isArtifact } = blocks[+idx]
|
|
473
|
-
blockIndex++
|
|
474
|
-
return buildCodeBlock(lang, code, uid(), false, ctx ? { ...ctx, blockIndex, isArtifact } : null)
|
|
475
|
-
})
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return DOMPurify.sanitize(html, PURIFY_OPTS)
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// ─────────────────────────────────────────────────────────────────
|
|
482
|
-
// 对外 API
|
|
483
|
-
// ─────────────────────────────────────────────────────────────────
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* 全量渲染
|
|
487
|
-
* @param {string} content
|
|
488
|
-
* @param {object} ctx - { messageId, artifactStore } 可选
|
|
489
|
-
*/
|
|
490
|
-
export function renderMarkdown(content, ctx = null) {
|
|
491
|
-
if (!content) return ''
|
|
492
|
-
return safeMarkdownParse(content, ctx)
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* 创建流式解析器(每条消息独享,ctx 在创建时绑定)
|
|
497
|
-
* @param {object} ctx - { messageId, artifactStore }
|
|
498
|
-
*/
|
|
499
|
-
export function createStreamingParser(ctx = null) {
|
|
500
|
-
const parser = new StreamingMarkdownParser(ctx)
|
|
501
|
-
return {
|
|
502
|
-
parse(content) { parser.reset(); return parser.parse(content) },
|
|
503
|
-
reset() { parser.reset() },
|
|
504
|
-
updateCtx(newCtx){ parser.ctx = newCtx },
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// ─────────────────────────────────────────────────────────────────
|
|
509
|
-
// sanitizeMermaidCode:预处理 mermaid 代码
|
|
510
|
-
// 把节点标签文本([] {} 内)里的圆括号转为 HTML 实体,
|
|
511
|
-
// 避免 AI 生成的 "(备注文字)" 被 mermaid 误解析为节点语法导致 Parse error
|
|
512
|
-
// ─────────────────────────────────────────────────────────────────
|
|
513
|
-
export function sanitizeMermaidCode(code) {
|
|
514
|
-
const lines = code.split('\n')
|
|
515
|
-
return lines.map(line => {
|
|
516
|
-
// 跳过注释行和图表类型声明行,不做处理
|
|
517
|
-
if (/^\s*%%/.test(line) || /^\s*(flowchart|graph|sequenceDiagram|classDiagram|stateDiagram|gantt|pie|gitGraph|erDiagram|journey|mindmap|timeline|quadrantChart|xychart|block|architecture|requirementDiagram)\b/.test(line)) {
|
|
518
|
-
return line
|
|
519
|
-
}
|
|
520
|
-
// [] 内文本的 () 转为 HTML 实体
|
|
521
|
-
return line
|
|
522
|
-
.replace(/\[([^\]]*)\]/g, (_, inner) =>
|
|
523
|
-
'[' + inner.replace(/\(/g, '(').replace(/\)/g, ')') + ']'
|
|
524
|
-
)
|
|
525
|
-
.replace(/\{([^}]*)\}/g, (_, inner) =>
|
|
526
|
-
'{' + inner.replace(/\(/g, '(').replace(/\)/g, ')') + '}'
|
|
527
|
-
)
|
|
528
|
-
}).join('\n')
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// ─────────────────────────────────────────────────────────────────
|
|
532
|
-
// postRender:Shiki + Mermaid
|
|
533
|
-
// ─────────────────────────────────────────────────────────────────
|
|
534
|
-
export async function postRender(el, { skipMermaid = false } = {}) {
|
|
535
|
-
if (!el) return
|
|
536
|
-
const tasks = []
|
|
537
|
-
|
|
538
|
-
const shikiEls = el.querySelectorAll('.shiki-live[data-id], .shiki-pre[data-id]')
|
|
539
|
-
if (shikiEls.length > 0) {
|
|
540
|
-
tasks.push(getShiki().then(hl => {
|
|
541
|
-
if (!hl) return
|
|
542
|
-
shikiEls.forEach(node => {
|
|
543
|
-
const id = node.dataset.id
|
|
544
|
-
const lang = node.dataset.lang || 'text'
|
|
545
|
-
const stored = pendingBlocks.get(id)
|
|
546
|
-
let code
|
|
547
|
-
if (stored?.code !== undefined) {
|
|
548
|
-
code = stored.code
|
|
549
|
-
} else if (node.dataset.code) {
|
|
550
|
-
try { code = decodeURIComponent(escape(atob(node.dataset.code))) } catch { code = node.textContent }
|
|
551
|
-
} else {
|
|
552
|
-
code = node.querySelector('code')?.textContent ?? node.textContent
|
|
553
|
-
}
|
|
554
|
-
// 语言不支持时直接降级,不传 'text' 给 Shiki(避免部分版本输出空内容)
|
|
555
|
-
if (!hl.getLoadedLanguages().includes(lang)) {
|
|
556
|
-
const fallback = document.createElement('pre')
|
|
557
|
-
fallback.className = 'shiki-pre'
|
|
558
|
-
fallback.style.cssText = 'margin:0;border-radius:0 0 8px 8px;font-size:13px;line-height:1.6;overflow-x:auto;padding:14px 16px'
|
|
559
|
-
fallback.innerHTML = `<code>${escapeHtml(code)}</code>`
|
|
560
|
-
node.replaceWith(fallback)
|
|
561
|
-
return
|
|
562
|
-
}
|
|
563
|
-
const safeLang = lang
|
|
564
|
-
// 始终尝试高亮,失败或结果为空时降级为带样式的 <pre>,绝不留白
|
|
565
|
-
const fallbackHtml = `<pre class="shiki-pre" style="margin:0;border-radius:0 0 8px 8px;font-size:13px;line-height:1.6;overflow-x:auto;padding:14px 16px"><code>${escapeHtml(code)}</code></pre>`
|
|
566
|
-
try {
|
|
567
|
-
const div = document.createElement('div')
|
|
568
|
-
div.innerHTML = hl.codeToHtml(code, { lang: safeLang, theme: currentTheme() })
|
|
569
|
-
const shikiNode = div.firstElementChild
|
|
570
|
-
if (shikiNode) {
|
|
571
|
-
shikiNode.style.cssText = 'margin:0;border-radius:0 0 8px 8px;font-size:13px;line-height:1.6;overflow-x:auto'
|
|
572
|
-
node.replaceWith(shikiNode)
|
|
573
|
-
} else {
|
|
574
|
-
// Shiki 返回空节点,用 fallback
|
|
575
|
-
const fallback = document.createElement('div')
|
|
576
|
-
fallback.innerHTML = fallbackHtml
|
|
577
|
-
node.replaceWith(fallback.firstElementChild)
|
|
578
|
-
}
|
|
579
|
-
} catch {
|
|
580
|
-
// Shiki 抛异常,用 fallback
|
|
581
|
-
const fallback = document.createElement('div')
|
|
582
|
-
fallback.innerHTML = fallbackHtml
|
|
583
|
-
node.replaceWith(fallback.firstElementChild)
|
|
584
|
-
}
|
|
585
|
-
})
|
|
586
|
-
}))
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
const mermaidEls = el.querySelectorAll('.mermaid-chart[data-mermaid]:not([data-rendered])')
|
|
590
|
-
if (mermaidEls.length > 0 && !skipMermaid) {
|
|
591
|
-
tasks.push(import('mermaid').then(({ default: mermaid }) => {
|
|
592
|
-
const isDark = document.documentElement.getAttribute('data-theme') !== 'light'
|
|
593
|
-
mermaid.initialize({ startOnLoad: false, suppressErrorRendering: true, theme: isDark ? 'dark' : 'default', fontFamily: "Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, 微软雅黑, Arial, sans-serif" })
|
|
594
|
-
mermaidEls.forEach(async el => {
|
|
595
|
-
const rawCode = decodeURIComponent(el.dataset.mermaid || '')
|
|
596
|
-
// 预处理:把节点标签文本([] {} 内)里的圆括号转为 HTML 实体
|
|
597
|
-
// 避免 AI 生成的 "(备注)" 被 mermaid 误解析为圆形节点语法导致 Parse error
|
|
598
|
-
const code = sanitizeMermaidCode(rawCode)
|
|
599
|
-
const id = 'mer-' + Math.random().toString(36).slice(2)
|
|
600
|
-
try {
|
|
601
|
-
const { svg } = await mermaid.render(id, code)
|
|
602
|
-
// 先在屏幕外解析 SVG,取真实高度,再平滑过渡,避免从占位高度突变到 SVG 高度
|
|
603
|
-
const tmp = document.createElement('div')
|
|
604
|
-
tmp.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;left:-9999px'
|
|
605
|
-
tmp.innerHTML = svg
|
|
606
|
-
document.body.appendChild(tmp)
|
|
607
|
-
const svgEl = tmp.querySelector('svg')
|
|
608
|
-
// 读取 SVG 自带 height 属性或 viewBox 推算,取不到则用 200px 兜底
|
|
609
|
-
let targetH = 200
|
|
610
|
-
if (svgEl) {
|
|
611
|
-
const h = svgEl.getAttribute('height')
|
|
612
|
-
if (h && parseFloat(h) > 0) {
|
|
613
|
-
targetH = Math.min(parseFloat(h), 800) // 超大图表最高限制 800px
|
|
614
|
-
} else {
|
|
615
|
-
const vb = svgEl.getAttribute('viewBox')
|
|
616
|
-
if (vb) {
|
|
617
|
-
const parts = vb.split(/[\s,]+/)
|
|
618
|
-
if (parts.length >= 4) targetH = Math.min(parseFloat(parts[3]) || 200, 800)
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
// 检查 mermaid 是否返回了错误 SVG(aria-roledescription="error")
|
|
623
|
-
// 11.x 有时不抛异常而是静默渲染一个错误占位 SVG
|
|
624
|
-
const isErrorSvg = svgEl?.getAttribute('aria-roledescription') === 'error'
|
|
625
|
-
document.body.removeChild(tmp)
|
|
626
|
-
|
|
627
|
-
if (isErrorSvg) {
|
|
628
|
-
el.classList.remove('mermaid-pending')
|
|
629
|
-
el.closest('.mermaid-wrapper')?.classList.add('mermaid-fallback')
|
|
630
|
-
el.innerHTML = `<pre class="mermaid-raw"><code>${escapeHtml(code)}</code></pre>`
|
|
631
|
-
console.warn('[mermaid] error svg detected, fallback to raw code')
|
|
632
|
-
return
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// 设置容器高度(平滑过渡到目标高度),再注入 SVG
|
|
636
|
-
el.style.minHeight = `${targetH}px`
|
|
637
|
-
el.style.transition = 'min-height 0.25s ease'
|
|
638
|
-
el.innerHTML = svg
|
|
639
|
-
el.classList.remove('mermaid-pending')
|
|
640
|
-
el.dataset.rendered = '1' // 标记已渲染,防止流式期间重复触发
|
|
641
|
-
// SVG 注入后 fade in
|
|
642
|
-
const svgNode = el.querySelector('svg')
|
|
643
|
-
if (svgNode) {
|
|
644
|
-
svgNode.style.opacity = '0'
|
|
645
|
-
svgNode.style.transition = 'opacity 0.2s ease'
|
|
646
|
-
requestAnimationFrame(() => { svgNode.style.opacity = '1' })
|
|
647
|
-
}
|
|
648
|
-
// 渲染完成后移除 min-height 约束,让 SVG 自然撑开
|
|
649
|
-
setTimeout(() => { el.style.minHeight = ''; el.style.transition = '' }, 300)
|
|
650
|
-
} catch (e) {
|
|
651
|
-
// 静默降级:不向用户暴露 mermaid 内部报错(如 "Syntax error in text")
|
|
652
|
-
// 改为折叠显示原始代码,用户可复制修正,不影响整体阅读体验
|
|
653
|
-
el.classList.remove('mermaid-pending')
|
|
654
|
-
const escaped = escapeHtml(code)
|
|
655
|
-
el.closest('.mermaid-wrapper')?.classList.add('mermaid-fallback')
|
|
656
|
-
el.innerHTML = `<pre class="mermaid-raw"><code>${escaped}</code></pre>`
|
|
657
|
-
console.warn('[mermaid] render failed:', e.message)
|
|
658
|
-
}
|
|
659
|
-
})
|
|
660
|
-
}))
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
await Promise.allSettled(tasks)
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// ─────────────────────────────────────────────────────────────────
|
|
667
|
-
// 模块级主题变更订阅器(单例 MutationObserver,避免每条消息都创建一个)
|
|
668
|
-
// MessageBubble 通过 onThemeChange / offThemeChange 订阅/取消订阅
|
|
669
|
-
// ─────────────────────────────────────────────────────────────────
|
|
670
|
-
const _themeSubscribers = new Set()
|
|
671
|
-
let _themeObserver = null
|
|
672
|
-
|
|
673
|
-
function ensureThemeObserver() {
|
|
674
|
-
if (_themeObserver) return
|
|
675
|
-
_themeObserver = new MutationObserver(() => {
|
|
676
|
-
_themeSubscribers.forEach(fn => fn())
|
|
677
|
-
})
|
|
678
|
-
_themeObserver.observe(document.documentElement, {
|
|
679
|
-
attributes: true, attributeFilter: ['data-theme'],
|
|
680
|
-
})
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
export function onThemeChange(fn) {
|
|
684
|
-
ensureThemeObserver()
|
|
685
|
-
_themeSubscribers.add(fn)
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
export function offThemeChange(fn) {
|
|
689
|
-
_themeSubscribers.delete(fn)
|
|
690
|
-
// 所有订阅者都退订时断开 Observer,释放资源
|
|
691
|
-
if (_themeSubscribers.size === 0 && _themeObserver) {
|
|
692
|
-
_themeObserver.disconnect()
|
|
693
|
-
_themeObserver = null
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// ─────────────────────────────────────────────────────────────────
|
|
698
|
-
// 代码块点击委托
|
|
699
|
-
// ─────────────────────────────────────────────────────────────────
|
|
700
|
-
export function handleCodeAction(e, openPreview) {
|
|
701
|
-
const btn = e.target.closest('[data-action]')
|
|
702
|
-
if (!btn) return
|
|
703
|
-
const { action, id } = btn.dataset
|
|
704
|
-
const block = pendingBlocks.get(id)
|
|
705
|
-
if (!block) return
|
|
706
|
-
|
|
707
|
-
if (action === 'copy') {
|
|
708
|
-
navigator.clipboard?.writeText(block.code).catch(() => {})
|
|
709
|
-
const orig = btn.innerHTML
|
|
710
|
-
btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#4ade80" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> 已复制`
|
|
711
|
-
btn.style.color = '#4ade80'
|
|
712
|
-
setTimeout(() => { btn.innerHTML = orig; btn.style.color = '' }, 2000)
|
|
713
|
-
}
|
|
714
|
-
if (action === 'preview' && openPreview) {
|
|
715
|
-
openPreview({ lang: block.lang, code: block.code })
|
|
716
|
-
}
|
|
717
|
-
if (action === 'zoom-mermaid' && openPreview) {
|
|
718
|
-
// 从 DOM 里取已渲染好的 SVG,传给 openPreview 展示全屏预览
|
|
719
|
-
const wrapper = btn.closest('.mermaid-wrapper')
|
|
720
|
-
const svgEl = wrapper?.querySelector('.mermaid-chart svg')
|
|
721
|
-
const svgHtml = svgEl ? svgEl.outerHTML : ''
|
|
722
|
-
if (svgHtml) openPreview({ lang: 'mermaid-svg', code: block.code, svgHtml })
|
|
723
|
-
}
|
|
724
|
-
}
|