md2ui 1.0.18 → 1.0.20
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/README.md +51 -58
- package/bin/build.js +95 -9
- package/bin/md2ui.js +102 -13
- package/package.json +24 -10
- package/public/docs/00-/345/277/253/351/200/237/345/274/200/345/247/213.md +48 -28
- package/public/docs/01-/345/212/237/350/203/275/347/211/271/346/200/247.md +55 -40
- package/public/docs/02-Markdown/346/270/262/346/237/223/00-/345/237/272/347/241/200/350/257/255/346/263/225.md +88 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/01-/344/273/243/347/240/201/345/235/227.md +91 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/02-/350/241/250/346/240/274.md +187 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/03-Mermaid/345/233/276/350/241/250.md +101 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/04-Frontmatter.md +32 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/05-/346/225/260/345/255/246/345/205/254/345/274/217.md +47 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/06-Mermaid/345/244/215/346/235/202/345/233/276/350/241/250/346/265/213/350/257/225.md +1376 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/assets/img-1777383093712.png +0 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/00-/344/270/211/346/240/217/345/270/203/345/261/200.md +33 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/01-/347/233/256/345/275/225/346/240/221/345/257/274/350/210/252.md +43 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/02-/346/226/207/346/241/243/345/244/247/347/272/262.md +51 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/03-/344/270/212/344/270/213/347/257/207/345/257/274/350/210/252.md +29 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/04-/347/253/231/345/206/205/351/223/276/346/216/245.md +39 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/05-/345/244/247/347/272/262/345/216/213/345/212/233/346/265/213/350/257/225.md +340 -0
- package/public/docs/04-/346/220/234/347/264/242/345/212/237/350/203/275/00-/345/205/250/346/226/207/346/220/234/347/264/242.md +46 -0
- package/public/docs/05-/347/274/226/350/276/221/345/212/237/350/203/275/00-/347/274/226/350/276/221/345/231/250/345/237/272/347/241/200.md +65 -0
- package/public/docs/05-/347/274/226/350/276/221/345/212/237/350/203/275/01-/350/207/252/345/212/250/344/277/235/345/255/230.md +38 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/00-/351/230/205/350/257/273/350/277/233/345/272/246.md +43 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/01-/345/233/276/347/211/207/346/224/276/345/244/247.md +40 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/02-/350/277/224/345/233/236/351/241/266/351/203/250.md +38 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/assets/img-1777261394722.png +0 -0
- package/public/docs/07-/347/247/273/345/212/250/347/253/257/351/200/202/351/205/215/00-/345/223/215/345/272/224/345/274/217/345/270/203/345/261/200.md +37 -0
- package/public/docs/08-/346/226/207/346/241/243/347/256/241/347/220/206/00-/346/226/260/345/273/272/344/270/216/345/210/240/351/231/244.md +47 -0
- package/public/docs/09-/345/257/274/345/207/272/345/212/237/350/203/275/00-/345/257/274/345/207/272Word.md +77 -0
- package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/00-CLI/345/267/245/345/205/267.md +52 -0
- package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/01-SSG/351/235/231/346/200/201/346/236/204/345/273/272.md +44 -0
- package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/02-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md +58 -0
- package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/00-/344/270/200/347/272/247/346/226/207/346/241/243.md +20 -0
- package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/01-/345/255/220/347/233/256/345/275/225/00-/344/272/214/347/272/247/346/226/207/346/241/243.md +13 -0
- package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/01-/345/255/220/347/233/256/345/275/225/01-/346/267/261/345/261/202/345/265/214/345/245/227/00-/344/270/211/347/272/247/346/226/207/346/241/243.md +23 -0
- package/src/App.vue +111 -12
- package/src/components/AppSidebar.vue +181 -21
- package/src/components/CodeBlockNodeView.vue +72 -0
- package/src/components/DocContent.vue +25 -14
- package/src/components/EditorContent.vue +257 -0
- package/src/components/EditorToolbar.vue +264 -0
- package/src/components/ImageZoom.vue +88 -5
- package/src/components/MathBlockNodeView.vue +160 -0
- package/src/components/MathInlineNodeView.vue +145 -0
- package/src/components/MermaidNodeView.vue +157 -0
- package/src/components/MobileSearch.vue +97 -0
- package/src/components/TableOfContents.vue +174 -32
- package/src/components/TopBar.vue +69 -4
- package/src/components/TreeNode.vue +232 -39
- package/src/components/WelcomePage.vue +2 -2
- package/src/composables/useDocHash.js +9 -1
- package/src/composables/useDocManager.js +452 -105
- package/src/composables/useDocTree.js +33 -2
- package/src/composables/useExportWord.js +73 -10
- package/src/composables/useFileWatcher.js +45 -0
- package/src/composables/useFrontmatter.js +2 -2
- package/src/composables/useMarkdown.js +450 -52
- package/src/composables/useMermaidCache.js +15 -0
- package/src/composables/useScroll.js +354 -27
- package/src/composables/useSearch.js +12 -11
- package/src/config.js +1 -4
- package/src/extensions/CodeBlockCustom.js +113 -0
- package/src/extensions/MathBlock.js +107 -0
- package/src/extensions/MathInline.js +100 -0
- package/src/extensions/MermaidBlock.js +73 -0
- package/src/extensions/TableControls.js +670 -0
- package/src/services/DocService.js +168 -0
- package/src/style.css +2416 -36
- package/src/utils/imageConverter.js +129 -0
- package/vite-plugin-doc-api.js +369 -0
- package/vite.config.js +7 -2
- package/public/docs/02-Mermaid/345/233/276/350/241/250.md +0 -102
- package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/01-/347/233/256/345/275/225/347/273/223/346/236/204.md +0 -55
- package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/02-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md +0 -63
- package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/03-/351/203/250/347/275/262/346/226/271/346/241/210.md +0 -73
- package/public/docs/04-API/345/217/202/350/200/203/01-/347/273/204/344/273/266API.md +0 -80
- package/public/docs/04-API/345/217/202/350/200/203/02-Composables.md +0 -92
- package/src/api/docs.js +0 -106
- package/src/components/SearchPanel.vue +0 -90
|
@@ -3,6 +3,14 @@ import { marked } from 'marked'
|
|
|
3
3
|
import mermaid from 'mermaid'
|
|
4
4
|
import GithubSlugger from 'github-slugger'
|
|
5
5
|
import hljs from 'highlight.js'
|
|
6
|
+
import katex from 'katex'
|
|
7
|
+
import 'katex/dist/katex.min.css'
|
|
8
|
+
|
|
9
|
+
// highlightjs-line-numbers.js 是 IIFE 插件,依赖全局 window.hljs
|
|
10
|
+
// 需先挂载 hljs 到 window,再动态加载插件
|
|
11
|
+
const lineNumbersReady = (typeof window !== 'undefined')
|
|
12
|
+
? (window.hljs = hljs, import('highlightjs-line-numbers.js'))
|
|
13
|
+
: Promise.resolve()
|
|
6
14
|
import { parseFrontmatter, calcReadingTime } from './useFrontmatter.js'
|
|
7
15
|
import { docHash, resolveDocKey, findDocInTree } from './useDocHash.js'
|
|
8
16
|
|
|
@@ -68,18 +76,18 @@ function createRenderer(currentDocKey, docsList) {
|
|
|
68
76
|
const renderer = new marked.Renderer()
|
|
69
77
|
const slugger = new GithubSlugger()
|
|
70
78
|
|
|
71
|
-
// 标题渲染:使用 github-slugger 生成语义化锚点 ID
|
|
79
|
+
// 标题渲染:使用 github-slugger 生成语义化锚点 ID,hover 显示 # 锚点链接
|
|
72
80
|
renderer.heading = function(text, level) {
|
|
73
81
|
const id = slugger.slug(text)
|
|
74
|
-
|
|
75
|
-
return `<h${level} id="${id}"><a class="heading-anchor" href="#${id}" data-anchor="${id}" aria-hidden="true">${linkIcon}</a>${text}</h${level}>\n`
|
|
82
|
+
return `<h${level} id="${id}"><a class="heading-anchor" href="#${id}" data-anchor="${id}" aria-hidden="true">#</a>${text}</h${level}>\n`
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
// 代码块渲染:Mermaid 图表 / 语法高亮 + 复制按钮
|
|
79
86
|
renderer.code = function(code, language) {
|
|
80
87
|
if (language === 'mermaid') {
|
|
81
88
|
const id = 'mermaid-' + Math.random().toString(36).substr(2, 9)
|
|
82
|
-
|
|
89
|
+
const encoded = encodeURIComponent(code)
|
|
90
|
+
return `<div class="mermaid mermaid-pending" id="${id}" data-source="${encoded}"><div class="mermaid-loading">渲染中...</div></div>`
|
|
83
91
|
}
|
|
84
92
|
// 高亮代码
|
|
85
93
|
let highlighted
|
|
@@ -89,33 +97,41 @@ function createRenderer(currentDocKey, docsList) {
|
|
|
89
97
|
highlighted = hljs.highlightAuto(code).value
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
// 生成行号(按换行拆分)
|
|
93
|
-
const lines = code.split('\n')
|
|
94
|
-
const lineCount = lines.length
|
|
95
|
-
const lineNums = lines.map((_, i) => `<span class="line-num">${i + 1}</span>`).join('')
|
|
96
|
-
|
|
97
100
|
const langLabel = (language || '').toUpperCase()
|
|
98
101
|
// 切换行号按钮(列表图标)
|
|
99
102
|
const toggleLineNumIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>`
|
|
103
|
+
// 切换自动换行按钮(换行图标)
|
|
104
|
+
const toggleWrapIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M3 12h15a3 3 0 1 1 0 6h-4"/><polyline points="13 16 11 18 13 20"/><path d="M3 18h4"/></svg>`
|
|
100
105
|
// 切换高亮按钮(</> 文本图标)
|
|
101
106
|
const toggleHighlightIcon = `<span class="code-icon-text"></></span>`
|
|
102
107
|
// 复制按钮图标
|
|
103
108
|
const copyIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`
|
|
104
109
|
|
|
105
|
-
return `<div class="code-block-wrapper" data-raw-code="${encodeURIComponent(code)}" data-lang="${language || ''}"
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
110
|
+
return `<div class="code-block-wrapper" data-raw-code="${encodeURIComponent(code)}" data-lang="${language || ''}">` +
|
|
111
|
+
`<div class="code-block-header">` +
|
|
112
|
+
`<span class="code-lang-label">${langLabel}</span>` +
|
|
113
|
+
`<div class="code-block-actions">` +
|
|
114
|
+
`<button class="code-action-btn toggle-line-num-btn active" data-tooltip="隐藏行号">${toggleLineNumIcon}</button>` +
|
|
115
|
+
`<button class="code-action-btn toggle-wrap-btn" data-tooltip="自动换行">${toggleWrapIcon}</button>` +
|
|
116
|
+
`<button class="code-action-btn toggle-highlight-btn active" data-tooltip="关闭高亮">${toggleHighlightIcon}</button>` +
|
|
117
|
+
`<button class="code-action-btn copy-code-btn" data-tooltip="复制代码">${copyIcon}<span class="copy-text">复制</span></button>` +
|
|
118
|
+
`</div>` +
|
|
119
|
+
`</div>` +
|
|
120
|
+
`<div class="code-block-body"><pre><code class="hljs${language ? ` language-${language}` : ''}">${highlighted}</code></pre></div>` +
|
|
121
|
+
`</div>`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 图片渲染:将相对路径转换为 /@user-docs/ 绝对路径
|
|
125
|
+
renderer.image = function(href, title, text) {
|
|
126
|
+
if (href && !/^(https?:|data:|\/|@)/.test(href)) {
|
|
127
|
+
// 相对路径图片,基于当前文档目录解析
|
|
128
|
+
const docDir = currentDocKey.includes('/') ? currentDocKey.substring(0, currentDocKey.lastIndexOf('/')) : ''
|
|
129
|
+
const base = docDir ? `/@user-docs/${docDir}/` : '/@user-docs/'
|
|
130
|
+
href = base + href
|
|
131
|
+
}
|
|
132
|
+
const alt = text || ''
|
|
133
|
+
const titleAttr = title ? ` title="${title}"` : ''
|
|
134
|
+
return `<img src="${href}" alt="${alt}"${titleAttr} class="zoomable-image" />`
|
|
119
135
|
}
|
|
120
136
|
|
|
121
137
|
// 链接渲染:站内/站外分类处理
|
|
@@ -152,70 +168,388 @@ function createRenderer(currentDocKey, docsList) {
|
|
|
152
168
|
return renderer
|
|
153
169
|
}
|
|
154
170
|
|
|
171
|
+
// ===== KaTeX 数学公式扩展 =====
|
|
172
|
+
// 为 marked 添加行内公式 $...$ 和块级公式 $$...$$ 支持
|
|
173
|
+
|
|
174
|
+
const mathInline = {
|
|
175
|
+
name: 'mathInline',
|
|
176
|
+
level: 'inline',
|
|
177
|
+
start(src) { return src.indexOf('$') },
|
|
178
|
+
tokenizer(src) {
|
|
179
|
+
// 行内公式:$...$(不匹配 $$)
|
|
180
|
+
const match = src.match(/^\$(?!\$)((?:\\.|[^$\\])+)\$/)
|
|
181
|
+
if (match) {
|
|
182
|
+
return { type: 'mathInline', raw: match[0], text: match[1].trim() }
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
renderer(token) {
|
|
186
|
+
try {
|
|
187
|
+
return katex.renderToString(token.text, { throwOnError: false, displayMode: false })
|
|
188
|
+
} catch {
|
|
189
|
+
return `<code class="math-error">${token.text}</code>`
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const mathBlock = {
|
|
195
|
+
name: 'mathBlock',
|
|
196
|
+
level: 'block',
|
|
197
|
+
start(src) {
|
|
198
|
+
// 查找行首的 $$(跳过行内代码中的 $$)
|
|
199
|
+
const match = src.match(/(?:^|\n)\$\$/)
|
|
200
|
+
return match ? match.index + (match[0].startsWith('\n') ? 1 : 0) : -1
|
|
201
|
+
},
|
|
202
|
+
tokenizer(src) {
|
|
203
|
+
// 块级公式:$$ 独占一行开始,$$ 独占一行结束
|
|
204
|
+
const match = src.match(/^\$\$\s*\n([\s\S]+?)\n\s*\$\$(?:\s*$|\n)/)
|
|
205
|
+
if (match) {
|
|
206
|
+
return { type: 'mathBlock', raw: match[0], text: match[1].trim() }
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
renderer(token) {
|
|
210
|
+
try {
|
|
211
|
+
return `<div class="math-block">${katex.renderToString(token.text, { throwOnError: false, displayMode: true })}</div>`
|
|
212
|
+
} catch {
|
|
213
|
+
return `<pre class="math-error">${token.text}</pre>`
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
155
218
|
|
|
156
219
|
// ---- 后处理器 ----
|
|
157
220
|
|
|
158
|
-
//
|
|
221
|
+
// 复制按钮旁边显示提示气泡
|
|
222
|
+
const COPY_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>'
|
|
223
|
+
const CHECK_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>'
|
|
224
|
+
|
|
225
|
+
function showCopyTip(btn, success) {
|
|
226
|
+
// 移除已有的 tip
|
|
227
|
+
const existing = btn.parentElement?.querySelector('.copy-tip')
|
|
228
|
+
if (existing) existing.remove()
|
|
229
|
+
const tip = document.createElement('span')
|
|
230
|
+
tip.className = 'copy-tip' + (success ? ' copy-tip-ok' : ' copy-tip-fail')
|
|
231
|
+
tip.textContent = success ? '已复制' : '失败'
|
|
232
|
+
// 插入到按钮后面
|
|
233
|
+
btn.parentElement.insertBefore(tip, btn.nextSibling)
|
|
234
|
+
btn.innerHTML = success ? CHECK_ICON : COPY_ICON
|
|
235
|
+
btn.classList.toggle('copied', success)
|
|
236
|
+
setTimeout(() => {
|
|
237
|
+
tip.remove()
|
|
238
|
+
btn.innerHTML = COPY_ICON
|
|
239
|
+
btn.classList.remove('copied')
|
|
240
|
+
}, 1500)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 图片格式转换工具(从共享模块导入)
|
|
244
|
+
import { imgToPngBlob, svgToPngBlob } from '../utils/imageConverter.js'
|
|
245
|
+
import { getMermaidCache, setMermaidCache } from './useMermaidCache.js'
|
|
246
|
+
|
|
247
|
+
// 渲染 Mermaid 图表(并行渲染所有图表,等全部完成后返回)
|
|
159
248
|
async function renderMermaid() {
|
|
160
249
|
await nextTick()
|
|
161
|
-
const mermaidElements = document.querySelectorAll('.mermaid')
|
|
162
|
-
|
|
250
|
+
const mermaidElements = document.querySelectorAll('.mermaid-pending')
|
|
251
|
+
if (!mermaidElements.length) return
|
|
252
|
+
|
|
253
|
+
// 所有图表并行渲染
|
|
254
|
+
const tasks = Array.from(mermaidElements).map(async (element) => {
|
|
255
|
+
const code = decodeURIComponent(element.dataset.source || '')
|
|
256
|
+
if (!code) return
|
|
257
|
+
const id = element.id
|
|
163
258
|
try {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
259
|
+
// 优先使用缓存
|
|
260
|
+
let svg = getMermaidCache(code)
|
|
261
|
+
if (!svg) {
|
|
262
|
+
const result = await mermaid.render(id + '-svg', code)
|
|
263
|
+
svg = result.svg
|
|
264
|
+
setMermaidCache(code, svg)
|
|
265
|
+
}
|
|
167
266
|
element.innerHTML = svg
|
|
267
|
+
element.classList.remove('mermaid-pending')
|
|
168
268
|
element.classList.add('zoomable-image')
|
|
169
269
|
element.style.cursor = 'zoom-in'
|
|
170
270
|
element.title = '点击放大查看'
|
|
271
|
+
// 添加复制按钮
|
|
272
|
+
element.style.position = 'relative'
|
|
273
|
+
const copyBtn = document.createElement('button')
|
|
274
|
+
copyBtn.className = 'mermaid-copy-btn image-copy-btn'
|
|
275
|
+
copyBtn.title = '复制图片'
|
|
276
|
+
copyBtn.innerHTML = COPY_ICON
|
|
277
|
+
copyBtn.addEventListener('click', async (e) => {
|
|
278
|
+
e.stopPropagation()
|
|
279
|
+
e.preventDefault()
|
|
280
|
+
try {
|
|
281
|
+
const svgEl = element.querySelector('svg')
|
|
282
|
+
if (!svgEl) return
|
|
283
|
+
const blobPromise = svgToPngBlob(svgEl, 2)
|
|
284
|
+
const item = new ClipboardItem({ 'image/png': blobPromise })
|
|
285
|
+
await navigator.clipboard.write([item])
|
|
286
|
+
showCopyTip(copyBtn, true)
|
|
287
|
+
} catch (err) {
|
|
288
|
+
console.warn('复制Mermaid图表失败:', err)
|
|
289
|
+
showCopyTip(copyBtn, false)
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
element.appendChild(copyBtn)
|
|
171
293
|
} catch (error) {
|
|
172
294
|
console.error('Mermaid 渲染失败:', error)
|
|
173
|
-
|
|
174
|
-
const errorEl = document.getElementById(element.id + '-svg')
|
|
295
|
+
const errorEl = document.getElementById(id + '-svg')
|
|
175
296
|
if (errorEl) errorEl.remove()
|
|
297
|
+
element.classList.remove('mermaid-pending')
|
|
176
298
|
element.innerHTML = `<pre class="mermaid-error">图表渲染失败\n${error.message}</pre>`
|
|
177
299
|
}
|
|
178
|
-
}
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
await Promise.all(tasks)
|
|
179
303
|
}
|
|
180
304
|
|
|
181
|
-
//
|
|
305
|
+
// 为表格添加滚动容器和工具栏按钮
|
|
182
306
|
function wrapTables() {
|
|
183
307
|
nextTick(() => {
|
|
184
308
|
const tables = document.querySelectorAll('.markdown-content table')
|
|
185
309
|
tables.forEach(table => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
310
|
+
// 已经处理过的跳过
|
|
311
|
+
if (table.closest('.table-outer')) return
|
|
312
|
+
|
|
313
|
+
// 代码块内的行号表格跳过
|
|
314
|
+
if (table.closest('.code-block-wrapper') || table.closest('pre')) return
|
|
315
|
+
|
|
316
|
+
// 构建结构:.table-outer > .table-toolbar + .table-wrapper > table
|
|
317
|
+
const outer = document.createElement('div')
|
|
318
|
+
outer.className = 'table-outer'
|
|
319
|
+
|
|
320
|
+
// 工具栏(在 wrapper 外部,不受 overflow 影响)
|
|
321
|
+
const toolbar = document.createElement('div')
|
|
322
|
+
toolbar.className = 'table-toolbar'
|
|
323
|
+
|
|
324
|
+
// SVG 图标定义
|
|
325
|
+
const icons = {
|
|
326
|
+
fixedWidth: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg>',
|
|
327
|
+
scrollX: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/><line x1="5" y1="5" x2="5" y2="19"/></svg>',
|
|
328
|
+
fixedHeight: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/></svg>',
|
|
329
|
+
autoHeight: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="7 8 12 3 17 8"/><polyline points="17 16 12 21 7 16"/><line x1="12" y1="3" x2="12" y2="21"/></svg>',
|
|
330
|
+
fullscreen: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>'
|
|
191
331
|
}
|
|
332
|
+
|
|
333
|
+
// 按钮配置:[key, tooltip, icon, group]
|
|
334
|
+
// group: 'width' 互斥组, 'height' 互斥组, null 独立
|
|
335
|
+
const btnConfigs = [
|
|
336
|
+
['fixedWidth', '固定宽度', icons.fixedWidth, 'width'],
|
|
337
|
+
['scrollX', '横向滚动', icons.scrollX, 'width'],
|
|
338
|
+
['fixedHeight', '固定高度', icons.fixedHeight, 'height'],
|
|
339
|
+
['autoHeight', '适应高度', icons.autoHeight, 'height'],
|
|
340
|
+
['fullscreen', '全屏查看', icons.fullscreen, null],
|
|
341
|
+
]
|
|
342
|
+
|
|
343
|
+
// 当前状态:默认固定宽度 + 适应高度
|
|
344
|
+
const state = { width: 'fixedWidth', height: 'autoHeight' }
|
|
345
|
+
|
|
346
|
+
const buttons = {}
|
|
347
|
+
|
|
348
|
+
btnConfigs.forEach(([key, tooltip, icon, group]) => {
|
|
349
|
+
const btn = document.createElement('button')
|
|
350
|
+
btn.className = 'table-toolbar-btn'
|
|
351
|
+
btn.dataset.tooltip = tooltip
|
|
352
|
+
btn.innerHTML = icon
|
|
353
|
+
|
|
354
|
+
// 默认激活状态
|
|
355
|
+
if ((group === 'width' && state.width === key) || (group === 'height' && state.height === key)) {
|
|
356
|
+
btn.classList.add('active')
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
btn.addEventListener('click', () => {
|
|
360
|
+
if (key === 'fullscreen') {
|
|
361
|
+
openTableFullscreen(table)
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
// 互斥切换
|
|
365
|
+
if (group) {
|
|
366
|
+
state[group] = key
|
|
367
|
+
// 更新同组按钮状态
|
|
368
|
+
btnConfigs.filter(([, , , g]) => g === group).forEach(([k]) => {
|
|
369
|
+
buttons[k].classList.toggle('active', k === key)
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
applyTableState(outer, wrapper, table, state)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
buttons[key] = btn
|
|
376
|
+
toolbar.appendChild(btn)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
// 滚动容器
|
|
380
|
+
const wrapper = document.createElement('div')
|
|
381
|
+
wrapper.className = 'table-wrapper'
|
|
382
|
+
|
|
383
|
+
table.parentNode.insertBefore(outer, table)
|
|
384
|
+
outer.appendChild(toolbar)
|
|
385
|
+
outer.appendChild(wrapper)
|
|
386
|
+
wrapper.appendChild(table)
|
|
387
|
+
|
|
388
|
+
// 应用默认状态
|
|
389
|
+
applyTableState(outer, wrapper, table, state)
|
|
192
390
|
})
|
|
193
391
|
})
|
|
194
392
|
}
|
|
195
393
|
|
|
394
|
+
// 根据状态应用表格样式
|
|
395
|
+
function applyTableState(outer, wrapper, table, state) {
|
|
396
|
+
// 宽度模式
|
|
397
|
+
table.classList.remove('table-fit', 'table-scroll')
|
|
398
|
+
wrapper.classList.remove('table-wrapper-scroll', 'table-wrapper-fixed')
|
|
399
|
+
if (state.width === 'fixedWidth') {
|
|
400
|
+
table.classList.add('table-fit')
|
|
401
|
+
wrapper.classList.add('table-wrapper-fixed')
|
|
402
|
+
} else {
|
|
403
|
+
table.classList.add('table-scroll')
|
|
404
|
+
wrapper.classList.add('table-wrapper-scroll')
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 高度模式
|
|
408
|
+
wrapper.classList.remove('table-wrapper-fixed-height', 'table-wrapper-auto-height')
|
|
409
|
+
if (state.height === 'fixedHeight') {
|
|
410
|
+
wrapper.classList.add('table-wrapper-fixed-height')
|
|
411
|
+
} else {
|
|
412
|
+
wrapper.classList.add('table-wrapper-auto-height')
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 打开表格全屏弹框
|
|
417
|
+
function openTableFullscreen(tableEl) {
|
|
418
|
+
// 创建遮罩
|
|
419
|
+
const overlay = document.createElement('div')
|
|
420
|
+
overlay.className = 'table-fullscreen-overlay'
|
|
421
|
+
|
|
422
|
+
// 弹框容器
|
|
423
|
+
const dialog = document.createElement('div')
|
|
424
|
+
dialog.className = 'table-fullscreen-dialog'
|
|
425
|
+
|
|
426
|
+
// 标题栏
|
|
427
|
+
const header = document.createElement('div')
|
|
428
|
+
header.className = 'table-fullscreen-header'
|
|
429
|
+
|
|
430
|
+
// 统计行列信息作为标题
|
|
431
|
+
const rowCount = tableEl.querySelectorAll('tr').length - 1
|
|
432
|
+
const firstRow = tableEl.querySelector('tr')
|
|
433
|
+
let colCount = 0
|
|
434
|
+
if (firstRow) {
|
|
435
|
+
for (const cell of firstRow.querySelectorAll('th, td')) {
|
|
436
|
+
colCount += parseInt(cell.getAttribute('colspan') || '1', 10)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const title = document.createElement('span')
|
|
440
|
+
title.className = 'table-fullscreen-title'
|
|
441
|
+
title.textContent = `表格预览(${rowCount} 行 × ${colCount} 列)`
|
|
442
|
+
|
|
443
|
+
const actions = document.createElement('div')
|
|
444
|
+
actions.className = 'table-fullscreen-actions'
|
|
445
|
+
|
|
446
|
+
// 全屏/还原按钮
|
|
447
|
+
const maximizeBtn = document.createElement('button')
|
|
448
|
+
maximizeBtn.className = 'table-fullscreen-action-btn'
|
|
449
|
+
maximizeBtn.title = '全屏'
|
|
450
|
+
const iconMaximize = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>'
|
|
451
|
+
const iconMinimize = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14h6v6"/><path d="M20 10h-6V4"/><path d="M14 10l7-7"/><path d="M3 21l7-7"/></svg>'
|
|
452
|
+
maximizeBtn.innerHTML = iconMaximize
|
|
453
|
+
maximizeBtn.addEventListener('click', () => {
|
|
454
|
+
const isMax = dialog.classList.toggle('is-maximized')
|
|
455
|
+
maximizeBtn.innerHTML = isMax ? iconMinimize : iconMaximize
|
|
456
|
+
maximizeBtn.title = isMax ? '还原' : '全屏'
|
|
457
|
+
overlay.style.padding = isMax ? '0' : '24px'
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
// 关闭按钮
|
|
461
|
+
const closeBtn = document.createElement('button')
|
|
462
|
+
closeBtn.className = 'table-fullscreen-action-btn'
|
|
463
|
+
closeBtn.title = '关闭'
|
|
464
|
+
closeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>'
|
|
465
|
+
|
|
466
|
+
actions.appendChild(maximizeBtn)
|
|
467
|
+
actions.appendChild(closeBtn)
|
|
468
|
+
header.appendChild(title)
|
|
469
|
+
header.appendChild(actions)
|
|
470
|
+
|
|
471
|
+
// 内容区
|
|
472
|
+
const body = document.createElement('div')
|
|
473
|
+
body.className = 'table-fullscreen-body'
|
|
474
|
+
body.appendChild(tableEl.cloneNode(true))
|
|
475
|
+
|
|
476
|
+
dialog.appendChild(header)
|
|
477
|
+
dialog.appendChild(body)
|
|
478
|
+
overlay.appendChild(dialog)
|
|
479
|
+
document.body.appendChild(overlay)
|
|
480
|
+
|
|
481
|
+
// 关闭逻辑
|
|
482
|
+
const close = () => overlay.remove()
|
|
483
|
+
closeBtn.addEventListener('click', close)
|
|
484
|
+
overlay.addEventListener('click', (e) => {
|
|
485
|
+
if (e.target === overlay) close()
|
|
486
|
+
})
|
|
487
|
+
const onKey = (e) => {
|
|
488
|
+
if (e.key === 'Escape') {
|
|
489
|
+
close()
|
|
490
|
+
document.removeEventListener('keydown', onKey)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
document.addEventListener('keydown', onKey)
|
|
494
|
+
}
|
|
495
|
+
|
|
196
496
|
// 为图片添加放大功能
|
|
197
497
|
function addImageZoomHandlers() {
|
|
198
498
|
nextTick(() => {
|
|
199
499
|
const images = document.querySelectorAll('.markdown-content img')
|
|
200
500
|
images.forEach(img => {
|
|
501
|
+
// 避免重复包裹
|
|
502
|
+
if (img.parentElement?.classList.contains('image-container')) return
|
|
201
503
|
img.classList.add('zoomable-image')
|
|
202
504
|
img.style.cursor = 'zoom-in'
|
|
203
505
|
img.title = '点击放大查看'
|
|
506
|
+
// 包裹容器,添加复制按钮
|
|
507
|
+
const wrapper = document.createElement('span')
|
|
508
|
+
wrapper.className = 'image-container'
|
|
509
|
+
img.parentNode.insertBefore(wrapper, img)
|
|
510
|
+
wrapper.appendChild(img)
|
|
511
|
+
const copyBtn = document.createElement('button')
|
|
512
|
+
copyBtn.className = 'image-copy-btn'
|
|
513
|
+
copyBtn.title = '复制图片'
|
|
514
|
+
copyBtn.innerHTML = COPY_ICON
|
|
515
|
+
copyBtn.addEventListener('click', async (e) => {
|
|
516
|
+
e.stopPropagation()
|
|
517
|
+
e.preventDefault()
|
|
518
|
+
try {
|
|
519
|
+
// 多策略获取 PNG blob,兼容同源和跨域图片
|
|
520
|
+
const blobPromise = imgToPngBlob(img.src)
|
|
521
|
+
const item = new ClipboardItem({ 'image/png': blobPromise })
|
|
522
|
+
await navigator.clipboard.write([item])
|
|
523
|
+
showCopyTip(copyBtn, true)
|
|
524
|
+
} catch (err) {
|
|
525
|
+
console.warn('复制图片失败:', err)
|
|
526
|
+
showCopyTip(copyBtn, false)
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
wrapper.appendChild(copyBtn)
|
|
204
530
|
})
|
|
205
531
|
})
|
|
206
532
|
}
|
|
207
533
|
|
|
208
534
|
// 为代码块添加交互事件(复制、切换行号、切换高亮)
|
|
209
535
|
function addCodeBlockHandlers() {
|
|
210
|
-
nextTick(() => {
|
|
536
|
+
nextTick(async () => {
|
|
537
|
+
// 等待插件加载完成
|
|
538
|
+
await lineNumbersReady
|
|
539
|
+
|
|
540
|
+
// 使用插件注入行号(table 布局,天然对齐)
|
|
541
|
+
document.querySelectorAll('.code-block-body code.hljs').forEach(block => {
|
|
542
|
+
hljs.lineNumbersBlock(block)
|
|
543
|
+
})
|
|
544
|
+
|
|
211
545
|
// 复制按钮
|
|
212
546
|
document.querySelectorAll('.copy-code-btn').forEach(btn => {
|
|
213
547
|
btn.addEventListener('click', async () => {
|
|
214
548
|
const wrapper = btn.closest('.code-block-wrapper')
|
|
215
|
-
const
|
|
216
|
-
if (!
|
|
549
|
+
const rawCode = decodeURIComponent(wrapper.dataset.rawCode || '')
|
|
550
|
+
if (!rawCode) return
|
|
217
551
|
try {
|
|
218
|
-
await navigator.clipboard.writeText(
|
|
552
|
+
await navigator.clipboard.writeText(rawCode)
|
|
219
553
|
const textEl = btn.querySelector('.copy-text')
|
|
220
554
|
textEl.textContent = '已复制'
|
|
221
555
|
btn.classList.add('copied')
|
|
@@ -224,26 +558,44 @@ function addCodeBlockHandlers() {
|
|
|
224
558
|
btn.classList.remove('copied')
|
|
225
559
|
}, 2000)
|
|
226
560
|
} catch {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
561
|
+
const code = wrapper.querySelector('code')
|
|
562
|
+
if (code) {
|
|
563
|
+
const range = document.createRange()
|
|
564
|
+
range.selectNodeContents(code)
|
|
565
|
+
window.getSelection().removeAllRanges()
|
|
566
|
+
window.getSelection().addRange(range)
|
|
567
|
+
}
|
|
231
568
|
}
|
|
232
569
|
})
|
|
233
570
|
})
|
|
234
571
|
|
|
235
|
-
//
|
|
572
|
+
// 切换行号(控制插件生成的 .hljs-ln 表格中行号列的显隐)
|
|
236
573
|
document.querySelectorAll('.toggle-line-num-btn').forEach(btn => {
|
|
237
574
|
btn.addEventListener('click', () => {
|
|
238
575
|
const wrapper = btn.closest('.code-block-wrapper')
|
|
239
|
-
const
|
|
240
|
-
if (!
|
|
576
|
+
const table = wrapper.querySelector('.hljs-ln')
|
|
577
|
+
if (!table) return
|
|
241
578
|
btn.classList.toggle('active')
|
|
242
|
-
|
|
579
|
+
// 切换所有行号单元格的显隐
|
|
580
|
+
table.querySelectorAll('.hljs-ln-numbers').forEach(td => {
|
|
581
|
+
td.style.display = btn.classList.contains('active') ? '' : 'none'
|
|
582
|
+
})
|
|
243
583
|
btn.dataset.tooltip = btn.classList.contains('active') ? '隐藏行号' : '显示行号'
|
|
244
584
|
})
|
|
245
585
|
})
|
|
246
586
|
|
|
587
|
+
// 切换自动换行
|
|
588
|
+
document.querySelectorAll('.toggle-wrap-btn').forEach(btn => {
|
|
589
|
+
btn.addEventListener('click', () => {
|
|
590
|
+
const wrapper = btn.closest('.code-block-wrapper')
|
|
591
|
+
const codeBody = wrapper.querySelector('.code-block-body')
|
|
592
|
+
if (!codeBody) return
|
|
593
|
+
btn.classList.toggle('active')
|
|
594
|
+
codeBody.classList.toggle('word-wrap-enabled')
|
|
595
|
+
btn.dataset.tooltip = btn.classList.contains('active') ? '取消换行' : '自动换行'
|
|
596
|
+
})
|
|
597
|
+
})
|
|
598
|
+
|
|
247
599
|
// 切换语法高亮
|
|
248
600
|
document.querySelectorAll('.toggle-highlight-btn').forEach(btn => {
|
|
249
601
|
btn.addEventListener('click', () => {
|
|
@@ -255,7 +607,7 @@ function addCodeBlockHandlers() {
|
|
|
255
607
|
const isHighlighted = btn.classList.contains('active')
|
|
256
608
|
|
|
257
609
|
if (isHighlighted) {
|
|
258
|
-
//
|
|
610
|
+
// 关闭高亮:显示纯文本,移除插件表格
|
|
259
611
|
codeEl.textContent = rawCode
|
|
260
612
|
codeEl.className = 'code-plain'
|
|
261
613
|
btn.classList.remove('active')
|
|
@@ -270,6 +622,8 @@ function addCodeBlockHandlers() {
|
|
|
270
622
|
}
|
|
271
623
|
codeEl.innerHTML = highlighted
|
|
272
624
|
codeEl.className = `hljs${lang ? ` language-${lang}` : ''}`
|
|
625
|
+
// 重新注入行号
|
|
626
|
+
hljs.lineNumbersBlock(codeEl)
|
|
273
627
|
btn.classList.add('active')
|
|
274
628
|
btn.dataset.tooltip = '关闭高亮'
|
|
275
629
|
}
|
|
@@ -296,6 +650,30 @@ function extractTOC(tocItems) {
|
|
|
296
650
|
})
|
|
297
651
|
}
|
|
298
652
|
|
|
653
|
+
// 从 Markdown 文本中解析 TOC(编辑模式使用,不依赖 DOM)
|
|
654
|
+
function extractTOCFromMarkdown(markdown, tocItems) {
|
|
655
|
+
const slugger = new GithubSlugger()
|
|
656
|
+
const items = []
|
|
657
|
+
const lines = markdown.split('\n')
|
|
658
|
+
let inCodeBlock = false
|
|
659
|
+
for (const line of lines) {
|
|
660
|
+
// 跳过代码块内的内容
|
|
661
|
+
if (/^```/.test(line.trim())) {
|
|
662
|
+
inCodeBlock = !inCodeBlock
|
|
663
|
+
continue
|
|
664
|
+
}
|
|
665
|
+
if (inCodeBlock) continue
|
|
666
|
+
const match = line.match(/^(#{1,6})\s+(.+)$/)
|
|
667
|
+
if (match) {
|
|
668
|
+
const level = match[1].length
|
|
669
|
+
const text = match[2].trim()
|
|
670
|
+
const id = slugger.slug(text)
|
|
671
|
+
items.push({ id, text, level })
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
tocItems.value = items
|
|
675
|
+
}
|
|
676
|
+
|
|
299
677
|
// ---- 主 composable ----
|
|
300
678
|
|
|
301
679
|
export function useMarkdown() {
|
|
@@ -304,8 +682,9 @@ export function useMarkdown() {
|
|
|
304
682
|
|
|
305
683
|
// 渲染 Markdown,传入当前文档 key 和文档列表用于链接改写
|
|
306
684
|
async function renderMarkdown(markdown, currentDocKey = '', docsList = []) {
|
|
307
|
-
const { data: frontmatter, content } = parseFrontmatter(markdown)
|
|
685
|
+
const { data: frontmatter, content, rawYaml } = parseFrontmatter(markdown)
|
|
308
686
|
const renderer = createRenderer(currentDocKey, docsList)
|
|
687
|
+
marked.use({ extensions: [mathBlock, mathInline] })
|
|
309
688
|
marked.setOptions({
|
|
310
689
|
renderer,
|
|
311
690
|
breaks: true,
|
|
@@ -332,6 +711,24 @@ export function useMarkdown() {
|
|
|
332
711
|
htmlContent.value = htmlContent.value.replace(/(<\/h1>)/, `$1\n${metaHtml}`)
|
|
333
712
|
}
|
|
334
713
|
|
|
714
|
+
// 在文档顶部插入 frontmatter 代码块
|
|
715
|
+
if (rawYaml) {
|
|
716
|
+
const highlighted = hljs.highlight(rawYaml, { language: 'yaml' }).value
|
|
717
|
+
const encodedRaw = encodeURIComponent(rawYaml)
|
|
718
|
+
const copyIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`
|
|
719
|
+
const frontmatterBlock =
|
|
720
|
+
`<div class="code-block-wrapper frontmatter-block" data-raw-code="${encodedRaw}" data-lang="yaml">` +
|
|
721
|
+
`<div class="code-block-header">` +
|
|
722
|
+
`<span class="code-lang-label">FRONTMATTER</span>` +
|
|
723
|
+
`<div class="code-block-actions">` +
|
|
724
|
+
`<button class="code-action-btn copy-code-btn" data-tooltip="复制代码">${copyIcon}<span class="copy-text">复制</span></button>` +
|
|
725
|
+
`</div>` +
|
|
726
|
+
`</div>` +
|
|
727
|
+
`<div class="code-block-body"><pre><code class="hljs language-yaml">${highlighted}</code></pre></div>` +
|
|
728
|
+
`</div>`
|
|
729
|
+
htmlContent.value = frontmatterBlock + htmlContent.value
|
|
730
|
+
}
|
|
731
|
+
|
|
335
732
|
// 后处理
|
|
336
733
|
await renderMermaid()
|
|
337
734
|
wrapTables()
|
|
@@ -344,6 +741,7 @@ export function useMarkdown() {
|
|
|
344
741
|
htmlContent,
|
|
345
742
|
tocItems,
|
|
346
743
|
renderMarkdown,
|
|
744
|
+
extractTOCFromMarkdown,
|
|
347
745
|
addImageZoomHandlers,
|
|
348
746
|
docHash
|
|
349
747
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Mermaid SVG 渲染缓存,以源码为 key,SVG 字符串为 value
|
|
2
|
+
const _cache = new Map()
|
|
3
|
+
|
|
4
|
+
export function getMermaidCache(code) {
|
|
5
|
+
return _cache.get(code.trim()) || null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function setMermaidCache(code, svg) {
|
|
9
|
+
_cache.set(code.trim(), svg)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// 切换文档时清空缓存,避免内存泄漏
|
|
13
|
+
export function clearMermaidCache() {
|
|
14
|
+
_cache.clear()
|
|
15
|
+
}
|