md2ui 1.0.16 → 1.0.19
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 +3 -55
- package/bin/build.js +82 -7
- package/bin/md2ui.js +80 -4
- package/package.json +23 -9
- 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 +86 -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/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/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 +130 -6
- 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 +199 -2
- package/src/components/MathBlockNodeView.vue +160 -0
- package/src/components/MathInlineNodeView.vue +145 -0
- package/src/components/MermaidNodeView.vue +149 -0
- package/src/components/TableBubbleMenu.vue +177 -0
- package/src/components/TableOfContents.vue +138 -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 +325 -68
- package/src/composables/useDocTree.js +56 -1
- package/src/composables/useExportPdf.js +102 -0
- 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 +529 -42
- package/src/composables/useScroll.js +47 -5
- package/src/config.js +1 -1
- 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 +184 -0
- package/src/style.css +2194 -39
- package/vite-plugin-doc-api.js +368 -0
- package/vite.config.js +2 -1
- 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
|
@@ -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,11 +76,10 @@ 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 图表 / 语法高亮 + 复制按钮
|
|
@@ -89,33 +96,41 @@ function createRenderer(currentDocKey, docsList) {
|
|
|
89
96
|
highlighted = hljs.highlightAuto(code).value
|
|
90
97
|
}
|
|
91
98
|
|
|
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
99
|
const langLabel = (language || '').toUpperCase()
|
|
98
100
|
// 切换行号按钮(列表图标)
|
|
99
101
|
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>`
|
|
102
|
+
// 切换自动换行按钮(换行图标)
|
|
103
|
+
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
104
|
// 切换高亮按钮(</> 文本图标)
|
|
101
105
|
const toggleHighlightIcon = `<span class="code-icon-text"></></span>`
|
|
102
106
|
// 复制按钮图标
|
|
103
107
|
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
108
|
|
|
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
|
-
|
|
109
|
+
return `<div class="code-block-wrapper" data-raw-code="${encodeURIComponent(code)}" data-lang="${language || ''}">` +
|
|
110
|
+
`<div class="code-block-header">` +
|
|
111
|
+
`<span class="code-lang-label">${langLabel}</span>` +
|
|
112
|
+
`<div class="code-block-actions">` +
|
|
113
|
+
`<button class="code-action-btn toggle-line-num-btn active" data-tooltip="隐藏行号">${toggleLineNumIcon}</button>` +
|
|
114
|
+
`<button class="code-action-btn toggle-wrap-btn" data-tooltip="自动换行">${toggleWrapIcon}</button>` +
|
|
115
|
+
`<button class="code-action-btn toggle-highlight-btn active" data-tooltip="关闭高亮">${toggleHighlightIcon}</button>` +
|
|
116
|
+
`<button class="code-action-btn copy-code-btn" data-tooltip="复制代码">${copyIcon}<span class="copy-text">复制</span></button>` +
|
|
117
|
+
`</div>` +
|
|
118
|
+
`</div>` +
|
|
119
|
+
`<div class="code-block-body"><pre><code class="hljs${language ? ` language-${language}` : ''}">${highlighted}</code></pre></div>` +
|
|
120
|
+
`</div>`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 图片渲染:将相对路径转换为 /@user-docs/ 绝对路径
|
|
124
|
+
renderer.image = function(href, title, text) {
|
|
125
|
+
if (href && !/^(https?:|data:|\/|@)/.test(href)) {
|
|
126
|
+
// 相对路径图片,基于当前文档目录解析
|
|
127
|
+
const docDir = currentDocKey.includes('/') ? currentDocKey.substring(0, currentDocKey.lastIndexOf('/')) : ''
|
|
128
|
+
const base = docDir ? `/@user-docs/${docDir}/` : '/@user-docs/'
|
|
129
|
+
href = base + href
|
|
130
|
+
}
|
|
131
|
+
const alt = text || ''
|
|
132
|
+
const titleAttr = title ? ` title="${title}"` : ''
|
|
133
|
+
return `<img src="${href}" alt="${alt}"${titleAttr} class="zoomable-image" />`
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
// 链接渲染:站内/站外分类处理
|
|
@@ -152,9 +167,185 @@ function createRenderer(currentDocKey, docsList) {
|
|
|
152
167
|
return renderer
|
|
153
168
|
}
|
|
154
169
|
|
|
170
|
+
// ===== KaTeX 数学公式扩展 =====
|
|
171
|
+
// 为 marked 添加行内公式 $...$ 和块级公式 $$...$$ 支持
|
|
172
|
+
|
|
173
|
+
const mathInline = {
|
|
174
|
+
name: 'mathInline',
|
|
175
|
+
level: 'inline',
|
|
176
|
+
start(src) { return src.indexOf('$') },
|
|
177
|
+
tokenizer(src) {
|
|
178
|
+
// 行内公式:$...$(不匹配 $$)
|
|
179
|
+
const match = src.match(/^\$(?!\$)((?:\\.|[^$\\])+)\$/)
|
|
180
|
+
if (match) {
|
|
181
|
+
return { type: 'mathInline', raw: match[0], text: match[1].trim() }
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
renderer(token) {
|
|
185
|
+
try {
|
|
186
|
+
return katex.renderToString(token.text, { throwOnError: false, displayMode: false })
|
|
187
|
+
} catch {
|
|
188
|
+
return `<code class="math-error">${token.text}</code>`
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const mathBlock = {
|
|
194
|
+
name: 'mathBlock',
|
|
195
|
+
level: 'block',
|
|
196
|
+
start(src) {
|
|
197
|
+
// 查找行首的 $$(跳过行内代码中的 $$)
|
|
198
|
+
const match = src.match(/(?:^|\n)\$\$/)
|
|
199
|
+
return match ? match.index + (match[0].startsWith('\n') ? 1 : 0) : -1
|
|
200
|
+
},
|
|
201
|
+
tokenizer(src) {
|
|
202
|
+
// 块级公式:$$ 独占一行开始,$$ 独占一行结束
|
|
203
|
+
const match = src.match(/^\$\$\s*\n([\s\S]+?)\n\s*\$\$(?:\s*$|\n)/)
|
|
204
|
+
if (match) {
|
|
205
|
+
return { type: 'mathBlock', raw: match[0], text: match[1].trim() }
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
renderer(token) {
|
|
209
|
+
try {
|
|
210
|
+
return `<div class="math-block">${katex.renderToString(token.text, { throwOnError: false, displayMode: true })}</div>`
|
|
211
|
+
} catch {
|
|
212
|
+
return `<pre class="math-error">${token.text}</pre>`
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
155
217
|
|
|
156
218
|
// ---- 后处理器 ----
|
|
157
219
|
|
|
220
|
+
// 复制按钮旁边显示提示气泡
|
|
221
|
+
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>'
|
|
222
|
+
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>'
|
|
223
|
+
|
|
224
|
+
function showCopyTip(btn, success) {
|
|
225
|
+
// 移除已有的 tip
|
|
226
|
+
const existing = btn.parentElement?.querySelector('.copy-tip')
|
|
227
|
+
if (existing) existing.remove()
|
|
228
|
+
const tip = document.createElement('span')
|
|
229
|
+
tip.className = 'copy-tip' + (success ? ' copy-tip-ok' : ' copy-tip-fail')
|
|
230
|
+
tip.textContent = success ? '已复制' : '失败'
|
|
231
|
+
// 插入到按钮后面
|
|
232
|
+
btn.parentElement.insertBefore(tip, btn.nextSibling)
|
|
233
|
+
btn.innerHTML = success ? CHECK_ICON : COPY_ICON
|
|
234
|
+
btn.classList.toggle('copied', success)
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
tip.remove()
|
|
237
|
+
btn.innerHTML = COPY_ICON
|
|
238
|
+
btn.classList.remove('copied')
|
|
239
|
+
}, 1500)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 将图片 URL 转为 PNG blob(兼容同源和跨域)
|
|
243
|
+
async function imgToPngBlob(src) {
|
|
244
|
+
// 策略1:fetch 获取(同源图片直接成功)
|
|
245
|
+
try {
|
|
246
|
+
const resp = await fetch(src)
|
|
247
|
+
const blob = await resp.blob()
|
|
248
|
+
if (blob.type === 'image/png') return blob
|
|
249
|
+
// 非 PNG 通过 canvas 转换
|
|
250
|
+
return new Promise((resolve, reject) => {
|
|
251
|
+
const url = URL.createObjectURL(blob)
|
|
252
|
+
const image = new Image()
|
|
253
|
+
image.onload = () => {
|
|
254
|
+
const canvas = document.createElement('canvas')
|
|
255
|
+
canvas.width = image.naturalWidth
|
|
256
|
+
canvas.height = image.naturalHeight
|
|
257
|
+
canvas.getContext('2d').drawImage(image, 0, 0)
|
|
258
|
+
URL.revokeObjectURL(url)
|
|
259
|
+
canvas.toBlob(b => b ? resolve(b) : reject(new Error('toBlob null')), 'image/png')
|
|
260
|
+
}
|
|
261
|
+
image.onerror = () => { URL.revokeObjectURL(url); reject(new Error('load fail')) }
|
|
262
|
+
image.src = url
|
|
263
|
+
})
|
|
264
|
+
} catch {
|
|
265
|
+
// 策略2:crossOrigin Image(需服务器支持 CORS)
|
|
266
|
+
try {
|
|
267
|
+
return await loadImageToBlob(src, true)
|
|
268
|
+
} catch {
|
|
269
|
+
// 策略3:不设 crossOrigin 直接画
|
|
270
|
+
return loadImageToBlob(src, false)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function loadImageToBlob(src, useCors) {
|
|
276
|
+
return new Promise((resolve, reject) => {
|
|
277
|
+
const image = new Image()
|
|
278
|
+
if (useCors) image.crossOrigin = 'anonymous'
|
|
279
|
+
image.onload = () => {
|
|
280
|
+
try {
|
|
281
|
+
const canvas = document.createElement('canvas')
|
|
282
|
+
canvas.width = image.naturalWidth
|
|
283
|
+
canvas.height = image.naturalHeight
|
|
284
|
+
canvas.getContext('2d').drawImage(image, 0, 0)
|
|
285
|
+
canvas.toBlob(b => b ? resolve(b) : reject(new Error('toBlob null')), 'image/png')
|
|
286
|
+
} catch (e) { reject(e) }
|
|
287
|
+
}
|
|
288
|
+
image.onerror = () => reject(new Error('图片加载失败'))
|
|
289
|
+
image.src = src
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// SVG 转 PNG blob(处理 foreignObject 导致的 tainted canvas 问题)
|
|
294
|
+
function svgToPngBlob(svgEl, scale = 2) {
|
|
295
|
+
return new Promise((resolve, reject) => {
|
|
296
|
+
const clone = svgEl.cloneNode(true)
|
|
297
|
+
// 从 viewBox 或属性中获取 SVG 实际尺寸,确保复制完整
|
|
298
|
+
const viewBox = clone.getAttribute('viewBox')
|
|
299
|
+
let svgWidth, svgHeight
|
|
300
|
+
if (viewBox) {
|
|
301
|
+
const parts = viewBox.split(/[\s,]+/)
|
|
302
|
+
svgWidth = parseFloat(parts[2])
|
|
303
|
+
svgHeight = parseFloat(parts[3])
|
|
304
|
+
}
|
|
305
|
+
if (!svgWidth || !svgHeight) {
|
|
306
|
+
svgWidth = parseFloat(clone.getAttribute('width')) || svgEl.getBoundingClientRect().width || 800
|
|
307
|
+
svgHeight = parseFloat(clone.getAttribute('height')) || svgEl.getBoundingClientRect().height || 600
|
|
308
|
+
}
|
|
309
|
+
// 显式设置 width/height,确保 Image 加载时尺寸正确
|
|
310
|
+
clone.setAttribute('width', svgWidth)
|
|
311
|
+
clone.setAttribute('height', svgHeight)
|
|
312
|
+
// 将 foreignObject 替换为 text 元素,避免 canvas 被污染
|
|
313
|
+
clone.querySelectorAll('foreignObject').forEach(fo => {
|
|
314
|
+
const text = fo.textContent.trim()
|
|
315
|
+
const x = fo.getAttribute('x') || '0'
|
|
316
|
+
const y = fo.getAttribute('y') || '0'
|
|
317
|
+
const width = fo.getAttribute('width') || '100'
|
|
318
|
+
const height = fo.getAttribute('height') || '20'
|
|
319
|
+
const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text')
|
|
320
|
+
textEl.setAttribute('x', String(parseFloat(x) + parseFloat(width) / 2))
|
|
321
|
+
textEl.setAttribute('y', String(parseFloat(y) + parseFloat(height) / 2 + 5))
|
|
322
|
+
textEl.setAttribute('text-anchor', 'middle')
|
|
323
|
+
textEl.setAttribute('font-size', '14')
|
|
324
|
+
textEl.setAttribute('fill', '#455a64')
|
|
325
|
+
textEl.textContent = text
|
|
326
|
+
fo.parentNode.replaceChild(textEl, fo)
|
|
327
|
+
})
|
|
328
|
+
const svgData = new XMLSerializer().serializeToString(clone)
|
|
329
|
+
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
|
|
330
|
+
const url = URL.createObjectURL(svgBlob)
|
|
331
|
+
const image = new Image()
|
|
332
|
+
image.onload = () => {
|
|
333
|
+
const w = svgWidth * scale
|
|
334
|
+
const h = svgHeight * scale
|
|
335
|
+
const canvas = document.createElement('canvas')
|
|
336
|
+
canvas.width = w
|
|
337
|
+
canvas.height = h
|
|
338
|
+
const ctx = canvas.getContext('2d')
|
|
339
|
+
ctx.scale(scale, scale)
|
|
340
|
+
ctx.drawImage(image, 0, 0, svgWidth, svgHeight)
|
|
341
|
+
URL.revokeObjectURL(url)
|
|
342
|
+
canvas.toBlob(b => b ? resolve(b) : reject(new Error('toBlob null')), 'image/png')
|
|
343
|
+
}
|
|
344
|
+
image.onerror = () => { URL.revokeObjectURL(url); reject(new Error('SVG加载失败')) }
|
|
345
|
+
image.src = url
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
158
349
|
// 渲染 Mermaid 图表
|
|
159
350
|
async function renderMermaid() {
|
|
160
351
|
await nextTick()
|
|
@@ -168,6 +359,31 @@ async function renderMermaid() {
|
|
|
168
359
|
element.classList.add('zoomable-image')
|
|
169
360
|
element.style.cursor = 'zoom-in'
|
|
170
361
|
element.title = '点击放大查看'
|
|
362
|
+
// 添加复制按钮
|
|
363
|
+
if (!element.querySelector('.mermaid-copy-btn')) {
|
|
364
|
+
element.style.position = 'relative'
|
|
365
|
+
const copyBtn = document.createElement('button')
|
|
366
|
+
copyBtn.className = 'mermaid-copy-btn image-copy-btn'
|
|
367
|
+
copyBtn.title = '复制图片'
|
|
368
|
+
copyBtn.innerHTML = COPY_ICON
|
|
369
|
+
copyBtn.addEventListener('click', async (e) => {
|
|
370
|
+
e.stopPropagation()
|
|
371
|
+
e.preventDefault()
|
|
372
|
+
try {
|
|
373
|
+
const svgEl = element.querySelector('svg')
|
|
374
|
+
if (!svgEl) return
|
|
375
|
+
// 克隆 SVG 并替换 foreignObject 为 text(避免 canvas tainted)
|
|
376
|
+
const blobPromise = svgToPngBlob(svgEl, 2)
|
|
377
|
+
const item = new ClipboardItem({ 'image/png': blobPromise })
|
|
378
|
+
await navigator.clipboard.write([item])
|
|
379
|
+
showCopyTip(copyBtn, true)
|
|
380
|
+
} catch (err) {
|
|
381
|
+
console.warn('复制Mermaid图表失败:', err)
|
|
382
|
+
showCopyTip(copyBtn, false)
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
element.appendChild(copyBtn)
|
|
386
|
+
}
|
|
171
387
|
} catch (error) {
|
|
172
388
|
console.error('Mermaid 渲染失败:', error)
|
|
173
389
|
// 清理 Mermaid 渲染失败时残留在 DOM 中的错误容器
|
|
@@ -178,44 +394,251 @@ async function renderMermaid() {
|
|
|
178
394
|
}
|
|
179
395
|
}
|
|
180
396
|
|
|
181
|
-
//
|
|
397
|
+
// 为表格添加滚动容器和工具栏按钮
|
|
182
398
|
function wrapTables() {
|
|
183
399
|
nextTick(() => {
|
|
184
400
|
const tables = document.querySelectorAll('.markdown-content table')
|
|
185
401
|
tables.forEach(table => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
402
|
+
// 已经处理过的跳过
|
|
403
|
+
if (table.closest('.table-outer')) return
|
|
404
|
+
|
|
405
|
+
// 构建结构:.table-outer > .table-toolbar + .table-wrapper > table
|
|
406
|
+
const outer = document.createElement('div')
|
|
407
|
+
outer.className = 'table-outer'
|
|
408
|
+
|
|
409
|
+
// 工具栏(在 wrapper 外部,不受 overflow 影响)
|
|
410
|
+
const toolbar = document.createElement('div')
|
|
411
|
+
toolbar.className = 'table-toolbar'
|
|
412
|
+
|
|
413
|
+
// SVG 图标定义
|
|
414
|
+
const icons = {
|
|
415
|
+
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>',
|
|
416
|
+
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>',
|
|
417
|
+
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>',
|
|
418
|
+
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>',
|
|
419
|
+
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
420
|
}
|
|
421
|
+
|
|
422
|
+
// 按钮配置:[key, tooltip, icon, group]
|
|
423
|
+
// group: 'width' 互斥组, 'height' 互斥组, null 独立
|
|
424
|
+
const btnConfigs = [
|
|
425
|
+
['fixedWidth', '固定宽度', icons.fixedWidth, 'width'],
|
|
426
|
+
['scrollX', '横向滚动', icons.scrollX, 'width'],
|
|
427
|
+
['fixedHeight', '固定高度', icons.fixedHeight, 'height'],
|
|
428
|
+
['autoHeight', '适应高度', icons.autoHeight, 'height'],
|
|
429
|
+
['fullscreen', '全屏查看', icons.fullscreen, null],
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
// 当前状态:默认固定宽度 + 适应高度
|
|
433
|
+
const state = { width: 'fixedWidth', height: 'autoHeight' }
|
|
434
|
+
|
|
435
|
+
const buttons = {}
|
|
436
|
+
|
|
437
|
+
btnConfigs.forEach(([key, tooltip, icon, group]) => {
|
|
438
|
+
const btn = document.createElement('button')
|
|
439
|
+
btn.className = 'table-toolbar-btn'
|
|
440
|
+
btn.dataset.tooltip = tooltip
|
|
441
|
+
btn.innerHTML = icon
|
|
442
|
+
|
|
443
|
+
// 默认激活状态
|
|
444
|
+
if ((group === 'width' && state.width === key) || (group === 'height' && state.height === key)) {
|
|
445
|
+
btn.classList.add('active')
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
btn.addEventListener('click', () => {
|
|
449
|
+
if (key === 'fullscreen') {
|
|
450
|
+
openTableFullscreen(table)
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
// 互斥切换
|
|
454
|
+
if (group) {
|
|
455
|
+
state[group] = key
|
|
456
|
+
// 更新同组按钮状态
|
|
457
|
+
btnConfigs.filter(([, , , g]) => g === group).forEach(([k]) => {
|
|
458
|
+
buttons[k].classList.toggle('active', k === key)
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
applyTableState(outer, wrapper, table, state)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
buttons[key] = btn
|
|
465
|
+
toolbar.appendChild(btn)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
// 滚动容器
|
|
469
|
+
const wrapper = document.createElement('div')
|
|
470
|
+
wrapper.className = 'table-wrapper'
|
|
471
|
+
|
|
472
|
+
table.parentNode.insertBefore(outer, table)
|
|
473
|
+
outer.appendChild(toolbar)
|
|
474
|
+
outer.appendChild(wrapper)
|
|
475
|
+
wrapper.appendChild(table)
|
|
476
|
+
|
|
477
|
+
// 应用默认状态
|
|
478
|
+
applyTableState(outer, wrapper, table, state)
|
|
192
479
|
})
|
|
193
480
|
})
|
|
194
481
|
}
|
|
195
482
|
|
|
483
|
+
// 根据状态应用表格样式
|
|
484
|
+
function applyTableState(outer, wrapper, table, state) {
|
|
485
|
+
// 宽度模式
|
|
486
|
+
table.classList.remove('table-fit', 'table-scroll')
|
|
487
|
+
wrapper.classList.remove('table-wrapper-scroll', 'table-wrapper-fixed')
|
|
488
|
+
if (state.width === 'fixedWidth') {
|
|
489
|
+
table.classList.add('table-fit')
|
|
490
|
+
wrapper.classList.add('table-wrapper-fixed')
|
|
491
|
+
} else {
|
|
492
|
+
table.classList.add('table-scroll')
|
|
493
|
+
wrapper.classList.add('table-wrapper-scroll')
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 高度模式
|
|
497
|
+
wrapper.classList.remove('table-wrapper-fixed-height', 'table-wrapper-auto-height')
|
|
498
|
+
if (state.height === 'fixedHeight') {
|
|
499
|
+
wrapper.classList.add('table-wrapper-fixed-height')
|
|
500
|
+
} else {
|
|
501
|
+
wrapper.classList.add('table-wrapper-auto-height')
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// 打开表格全屏弹框
|
|
506
|
+
function openTableFullscreen(tableEl) {
|
|
507
|
+
// 创建遮罩
|
|
508
|
+
const overlay = document.createElement('div')
|
|
509
|
+
overlay.className = 'table-fullscreen-overlay'
|
|
510
|
+
|
|
511
|
+
// 弹框容器
|
|
512
|
+
const dialog = document.createElement('div')
|
|
513
|
+
dialog.className = 'table-fullscreen-dialog'
|
|
514
|
+
|
|
515
|
+
// 标题栏
|
|
516
|
+
const header = document.createElement('div')
|
|
517
|
+
header.className = 'table-fullscreen-header'
|
|
518
|
+
|
|
519
|
+
// 统计行列信息作为标题
|
|
520
|
+
const rowCount = tableEl.querySelectorAll('tr').length - 1
|
|
521
|
+
const firstRow = tableEl.querySelector('tr')
|
|
522
|
+
let colCount = 0
|
|
523
|
+
if (firstRow) {
|
|
524
|
+
for (const cell of firstRow.querySelectorAll('th, td')) {
|
|
525
|
+
colCount += parseInt(cell.getAttribute('colspan') || '1', 10)
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const title = document.createElement('span')
|
|
529
|
+
title.className = 'table-fullscreen-title'
|
|
530
|
+
title.textContent = `表格预览(${rowCount} 行 × ${colCount} 列)`
|
|
531
|
+
|
|
532
|
+
const actions = document.createElement('div')
|
|
533
|
+
actions.className = 'table-fullscreen-actions'
|
|
534
|
+
|
|
535
|
+
// 全屏/还原按钮
|
|
536
|
+
const maximizeBtn = document.createElement('button')
|
|
537
|
+
maximizeBtn.className = 'table-fullscreen-action-btn'
|
|
538
|
+
maximizeBtn.title = '全屏'
|
|
539
|
+
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>'
|
|
540
|
+
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>'
|
|
541
|
+
maximizeBtn.innerHTML = iconMaximize
|
|
542
|
+
maximizeBtn.addEventListener('click', () => {
|
|
543
|
+
const isMax = dialog.classList.toggle('is-maximized')
|
|
544
|
+
maximizeBtn.innerHTML = isMax ? iconMinimize : iconMaximize
|
|
545
|
+
maximizeBtn.title = isMax ? '还原' : '全屏'
|
|
546
|
+
overlay.style.padding = isMax ? '0' : '24px'
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
// 关闭按钮
|
|
550
|
+
const closeBtn = document.createElement('button')
|
|
551
|
+
closeBtn.className = 'table-fullscreen-action-btn'
|
|
552
|
+
closeBtn.title = '关闭'
|
|
553
|
+
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>'
|
|
554
|
+
|
|
555
|
+
actions.appendChild(maximizeBtn)
|
|
556
|
+
actions.appendChild(closeBtn)
|
|
557
|
+
header.appendChild(title)
|
|
558
|
+
header.appendChild(actions)
|
|
559
|
+
|
|
560
|
+
// 内容区
|
|
561
|
+
const body = document.createElement('div')
|
|
562
|
+
body.className = 'table-fullscreen-body'
|
|
563
|
+
body.appendChild(tableEl.cloneNode(true))
|
|
564
|
+
|
|
565
|
+
dialog.appendChild(header)
|
|
566
|
+
dialog.appendChild(body)
|
|
567
|
+
overlay.appendChild(dialog)
|
|
568
|
+
document.body.appendChild(overlay)
|
|
569
|
+
|
|
570
|
+
// 关闭逻辑
|
|
571
|
+
const close = () => overlay.remove()
|
|
572
|
+
closeBtn.addEventListener('click', close)
|
|
573
|
+
overlay.addEventListener('click', (e) => {
|
|
574
|
+
if (e.target === overlay) close()
|
|
575
|
+
})
|
|
576
|
+
const onKey = (e) => {
|
|
577
|
+
if (e.key === 'Escape') {
|
|
578
|
+
close()
|
|
579
|
+
document.removeEventListener('keydown', onKey)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
document.addEventListener('keydown', onKey)
|
|
583
|
+
}
|
|
584
|
+
|
|
196
585
|
// 为图片添加放大功能
|
|
197
586
|
function addImageZoomHandlers() {
|
|
198
587
|
nextTick(() => {
|
|
199
588
|
const images = document.querySelectorAll('.markdown-content img')
|
|
200
589
|
images.forEach(img => {
|
|
590
|
+
// 避免重复包裹
|
|
591
|
+
if (img.parentElement?.classList.contains('image-container')) return
|
|
201
592
|
img.classList.add('zoomable-image')
|
|
202
593
|
img.style.cursor = 'zoom-in'
|
|
203
594
|
img.title = '点击放大查看'
|
|
595
|
+
// 包裹容器,添加复制按钮
|
|
596
|
+
const wrapper = document.createElement('span')
|
|
597
|
+
wrapper.className = 'image-container'
|
|
598
|
+
img.parentNode.insertBefore(wrapper, img)
|
|
599
|
+
wrapper.appendChild(img)
|
|
600
|
+
const copyBtn = document.createElement('button')
|
|
601
|
+
copyBtn.className = 'image-copy-btn'
|
|
602
|
+
copyBtn.title = '复制图片'
|
|
603
|
+
copyBtn.innerHTML = COPY_ICON
|
|
604
|
+
copyBtn.addEventListener('click', async (e) => {
|
|
605
|
+
e.stopPropagation()
|
|
606
|
+
e.preventDefault()
|
|
607
|
+
try {
|
|
608
|
+
// 多策略获取 PNG blob,兼容同源和跨域图片
|
|
609
|
+
const blobPromise = imgToPngBlob(img.src)
|
|
610
|
+
const item = new ClipboardItem({ 'image/png': blobPromise })
|
|
611
|
+
await navigator.clipboard.write([item])
|
|
612
|
+
showCopyTip(copyBtn, true)
|
|
613
|
+
} catch (err) {
|
|
614
|
+
console.warn('复制图片失败:', err)
|
|
615
|
+
showCopyTip(copyBtn, false)
|
|
616
|
+
}
|
|
617
|
+
})
|
|
618
|
+
wrapper.appendChild(copyBtn)
|
|
204
619
|
})
|
|
205
620
|
})
|
|
206
621
|
}
|
|
207
622
|
|
|
208
623
|
// 为代码块添加交互事件(复制、切换行号、切换高亮)
|
|
209
624
|
function addCodeBlockHandlers() {
|
|
210
|
-
nextTick(() => {
|
|
625
|
+
nextTick(async () => {
|
|
626
|
+
// 等待插件加载完成
|
|
627
|
+
await lineNumbersReady
|
|
628
|
+
|
|
629
|
+
// 使用插件注入行号(table 布局,天然对齐)
|
|
630
|
+
document.querySelectorAll('.code-block-body code.hljs').forEach(block => {
|
|
631
|
+
hljs.lineNumbersBlock(block)
|
|
632
|
+
})
|
|
633
|
+
|
|
211
634
|
// 复制按钮
|
|
212
635
|
document.querySelectorAll('.copy-code-btn').forEach(btn => {
|
|
213
636
|
btn.addEventListener('click', async () => {
|
|
214
637
|
const wrapper = btn.closest('.code-block-wrapper')
|
|
215
|
-
const
|
|
216
|
-
if (!
|
|
638
|
+
const rawCode = decodeURIComponent(wrapper.dataset.rawCode || '')
|
|
639
|
+
if (!rawCode) return
|
|
217
640
|
try {
|
|
218
|
-
await navigator.clipboard.writeText(
|
|
641
|
+
await navigator.clipboard.writeText(rawCode)
|
|
219
642
|
const textEl = btn.querySelector('.copy-text')
|
|
220
643
|
textEl.textContent = '已复制'
|
|
221
644
|
btn.classList.add('copied')
|
|
@@ -224,26 +647,44 @@ function addCodeBlockHandlers() {
|
|
|
224
647
|
btn.classList.remove('copied')
|
|
225
648
|
}, 2000)
|
|
226
649
|
} catch {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
650
|
+
const code = wrapper.querySelector('code')
|
|
651
|
+
if (code) {
|
|
652
|
+
const range = document.createRange()
|
|
653
|
+
range.selectNodeContents(code)
|
|
654
|
+
window.getSelection().removeAllRanges()
|
|
655
|
+
window.getSelection().addRange(range)
|
|
656
|
+
}
|
|
231
657
|
}
|
|
232
658
|
})
|
|
233
659
|
})
|
|
234
660
|
|
|
235
|
-
//
|
|
661
|
+
// 切换行号(控制插件生成的 .hljs-ln 表格中行号列的显隐)
|
|
236
662
|
document.querySelectorAll('.toggle-line-num-btn').forEach(btn => {
|
|
237
663
|
btn.addEventListener('click', () => {
|
|
238
664
|
const wrapper = btn.closest('.code-block-wrapper')
|
|
239
|
-
const
|
|
240
|
-
if (!
|
|
665
|
+
const table = wrapper.querySelector('.hljs-ln')
|
|
666
|
+
if (!table) return
|
|
241
667
|
btn.classList.toggle('active')
|
|
242
|
-
|
|
668
|
+
// 切换所有行号单元格的显隐
|
|
669
|
+
table.querySelectorAll('.hljs-ln-numbers').forEach(td => {
|
|
670
|
+
td.style.display = btn.classList.contains('active') ? '' : 'none'
|
|
671
|
+
})
|
|
243
672
|
btn.dataset.tooltip = btn.classList.contains('active') ? '隐藏行号' : '显示行号'
|
|
244
673
|
})
|
|
245
674
|
})
|
|
246
675
|
|
|
676
|
+
// 切换自动换行
|
|
677
|
+
document.querySelectorAll('.toggle-wrap-btn').forEach(btn => {
|
|
678
|
+
btn.addEventListener('click', () => {
|
|
679
|
+
const wrapper = btn.closest('.code-block-wrapper')
|
|
680
|
+
const codeBody = wrapper.querySelector('.code-block-body')
|
|
681
|
+
if (!codeBody) return
|
|
682
|
+
btn.classList.toggle('active')
|
|
683
|
+
codeBody.classList.toggle('word-wrap-enabled')
|
|
684
|
+
btn.dataset.tooltip = btn.classList.contains('active') ? '取消换行' : '自动换行'
|
|
685
|
+
})
|
|
686
|
+
})
|
|
687
|
+
|
|
247
688
|
// 切换语法高亮
|
|
248
689
|
document.querySelectorAll('.toggle-highlight-btn').forEach(btn => {
|
|
249
690
|
btn.addEventListener('click', () => {
|
|
@@ -255,7 +696,7 @@ function addCodeBlockHandlers() {
|
|
|
255
696
|
const isHighlighted = btn.classList.contains('active')
|
|
256
697
|
|
|
257
698
|
if (isHighlighted) {
|
|
258
|
-
//
|
|
699
|
+
// 关闭高亮:显示纯文本,移除插件表格
|
|
259
700
|
codeEl.textContent = rawCode
|
|
260
701
|
codeEl.className = 'code-plain'
|
|
261
702
|
btn.classList.remove('active')
|
|
@@ -270,6 +711,8 @@ function addCodeBlockHandlers() {
|
|
|
270
711
|
}
|
|
271
712
|
codeEl.innerHTML = highlighted
|
|
272
713
|
codeEl.className = `hljs${lang ? ` language-${lang}` : ''}`
|
|
714
|
+
// 重新注入行号
|
|
715
|
+
hljs.lineNumbersBlock(codeEl)
|
|
273
716
|
btn.classList.add('active')
|
|
274
717
|
btn.dataset.tooltip = '关闭高亮'
|
|
275
718
|
}
|
|
@@ -296,6 +739,30 @@ function extractTOC(tocItems) {
|
|
|
296
739
|
})
|
|
297
740
|
}
|
|
298
741
|
|
|
742
|
+
// 从 Markdown 文本中解析 TOC(编辑模式使用,不依赖 DOM)
|
|
743
|
+
function extractTOCFromMarkdown(markdown, tocItems) {
|
|
744
|
+
const slugger = new GithubSlugger()
|
|
745
|
+
const items = []
|
|
746
|
+
const lines = markdown.split('\n')
|
|
747
|
+
let inCodeBlock = false
|
|
748
|
+
for (const line of lines) {
|
|
749
|
+
// 跳过代码块内的内容
|
|
750
|
+
if (/^```/.test(line.trim())) {
|
|
751
|
+
inCodeBlock = !inCodeBlock
|
|
752
|
+
continue
|
|
753
|
+
}
|
|
754
|
+
if (inCodeBlock) continue
|
|
755
|
+
const match = line.match(/^(#{1,6})\s+(.+)$/)
|
|
756
|
+
if (match) {
|
|
757
|
+
const level = match[1].length
|
|
758
|
+
const text = match[2].trim()
|
|
759
|
+
const id = slugger.slug(text)
|
|
760
|
+
items.push({ id, text, level })
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
tocItems.value = items
|
|
764
|
+
}
|
|
765
|
+
|
|
299
766
|
// ---- 主 composable ----
|
|
300
767
|
|
|
301
768
|
export function useMarkdown() {
|
|
@@ -304,8 +771,9 @@ export function useMarkdown() {
|
|
|
304
771
|
|
|
305
772
|
// 渲染 Markdown,传入当前文档 key 和文档列表用于链接改写
|
|
306
773
|
async function renderMarkdown(markdown, currentDocKey = '', docsList = []) {
|
|
307
|
-
const { data: frontmatter, content } = parseFrontmatter(markdown)
|
|
774
|
+
const { data: frontmatter, content, rawYaml } = parseFrontmatter(markdown)
|
|
308
775
|
const renderer = createRenderer(currentDocKey, docsList)
|
|
776
|
+
marked.use({ extensions: [mathBlock, mathInline] })
|
|
309
777
|
marked.setOptions({
|
|
310
778
|
renderer,
|
|
311
779
|
breaks: true,
|
|
@@ -332,6 +800,24 @@ export function useMarkdown() {
|
|
|
332
800
|
htmlContent.value = htmlContent.value.replace(/(<\/h1>)/, `$1\n${metaHtml}`)
|
|
333
801
|
}
|
|
334
802
|
|
|
803
|
+
// 在文档顶部插入 frontmatter 代码块
|
|
804
|
+
if (rawYaml) {
|
|
805
|
+
const highlighted = hljs.highlight(rawYaml, { language: 'yaml' }).value
|
|
806
|
+
const encodedRaw = encodeURIComponent(rawYaml)
|
|
807
|
+
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>`
|
|
808
|
+
const frontmatterBlock =
|
|
809
|
+
`<div class="code-block-wrapper frontmatter-block" data-raw-code="${encodedRaw}" data-lang="yaml">` +
|
|
810
|
+
`<div class="code-block-header">` +
|
|
811
|
+
`<span class="code-lang-label">FRONTMATTER</span>` +
|
|
812
|
+
`<div class="code-block-actions">` +
|
|
813
|
+
`<button class="code-action-btn copy-code-btn" data-tooltip="复制代码">${copyIcon}<span class="copy-text">复制</span></button>` +
|
|
814
|
+
`</div>` +
|
|
815
|
+
`</div>` +
|
|
816
|
+
`<div class="code-block-body"><pre><code class="hljs language-yaml">${highlighted}</code></pre></div>` +
|
|
817
|
+
`</div>`
|
|
818
|
+
htmlContent.value = frontmatterBlock + htmlContent.value
|
|
819
|
+
}
|
|
820
|
+
|
|
335
821
|
// 后处理
|
|
336
822
|
await renderMermaid()
|
|
337
823
|
wrapTables()
|
|
@@ -344,6 +830,7 @@ export function useMarkdown() {
|
|
|
344
830
|
htmlContent,
|
|
345
831
|
tocItems,
|
|
346
832
|
renderMarkdown,
|
|
833
|
+
extractTOCFromMarkdown,
|
|
347
834
|
addImageZoomHandlers,
|
|
348
835
|
docHash
|
|
349
836
|
}
|