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
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# md2ui
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/md2ui)
|
|
4
|
-
[](https://github.com/xiaoyaodev/md2ui)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
7
|
一个轻量级的 Markdown 文档渲染工具,将本地 `.md` 文件转换为美观的 HTML 页面。支持实时预览和静态站点生成(SSG)两种模式。
|
|
@@ -12,62 +12,11 @@
|
|
|
12
12
|
- 三栏布局 - 左侧导航 / 中间内容 / 右侧大纲,可拖拽调整宽度
|
|
13
13
|
- Markdown 增强 - GFM 语法、代码高亮、Mermaid 图表、Frontmatter
|
|
14
14
|
- 全文搜索 - 基于 MiniSearch 的快速全文检索
|
|
15
|
-
- 暗色模式 - 一键切换亮色/暗色主题
|
|
16
15
|
- 移动端适配 - 响应式布局,抽屉式导航和目录
|
|
17
16
|
- 阅读体验 - 阅读进度条、预计阅读时间、上下篇导航
|
|
18
17
|
- SSG 构建 - `md2ui build` 生成纯静态 HTML,可直接部署
|
|
19
18
|
- 自定义配置 - 站点标题、主题色、GitHub 链接、页脚等
|
|
20
19
|
|
|
21
|
-
## 效果预览
|
|
22
|
-
|
|
23
|
-
### 全文搜索
|
|
24
|
-
|
|
25
|
-

|
|
26
|
-
|
|
27
|
-
### 代码高亮
|
|
28
|
-
|
|
29
|
-

|
|
30
|
-
|
|
31
|
-

|
|
32
|
-
|
|
33
|
-
### 暗色模式
|
|
34
|
-
|
|
35
|
-

|
|
36
|
-
|
|
37
|
-

|
|
38
|
-
|
|
39
|
-
### 移动端适配
|
|
40
|
-
|
|
41
|
-
<p>
|
|
42
|
-
<img src="screenshots/task4-mobile-welcome.png" width="32%" alt="移动端 - 欢迎页" />
|
|
43
|
-
<img src="screenshots/task4-mobile-drawer.png" width="32%" alt="移动端 - 抽屉导航" />
|
|
44
|
-
<img src="screenshots/task4-mobile-toc.png" width="32%" alt="移动端 - 目录大纲" />
|
|
45
|
-
</p>
|
|
46
|
-
|
|
47
|
-

|
|
48
|
-
|
|
49
|
-
### 上下篇导航
|
|
50
|
-
|
|
51
|
-

|
|
52
|
-
|
|
53
|
-
### 阅读时间
|
|
54
|
-
|
|
55
|
-

|
|
56
|
-
|
|
57
|
-
### Frontmatter 支持
|
|
58
|
-
|
|
59
|
-

|
|
60
|
-
|
|
61
|
-
### 导航过滤
|
|
62
|
-
|
|
63
|
-

|
|
64
|
-
|
|
65
|
-
### SSG 静态构建
|
|
66
|
-
|
|
67
|
-

|
|
68
|
-
|
|
69
|
-

|
|
70
|
-
|
|
71
20
|
## 安装使用
|
|
72
21
|
|
|
73
22
|
### 全局安装
|
|
@@ -130,7 +79,7 @@ export default {
|
|
|
130
79
|
port: 8080,
|
|
131
80
|
folderExpanded: true,
|
|
132
81
|
themeColor: '#3eaf7c',
|
|
133
|
-
github: 'https://github.com/
|
|
82
|
+
github: 'https://github.com/xiaoyaodev/md2ui',
|
|
134
83
|
footer: 'Copyright © 2025'
|
|
135
84
|
}
|
|
136
85
|
```
|
|
@@ -147,7 +96,7 @@ export default {
|
|
|
147
96
|
## 开发
|
|
148
97
|
|
|
149
98
|
```bash
|
|
150
|
-
git clone https://github.com/
|
|
99
|
+
git clone https://github.com/xiaoyaodev/md2ui.git
|
|
151
100
|
cd md2ui
|
|
152
101
|
pnpm install
|
|
153
102
|
pnpm dev
|
|
@@ -162,7 +111,6 @@ md2ui/
|
|
|
162
111
|
│ └── build.js # SSG 静态构建
|
|
163
112
|
├── src/
|
|
164
113
|
│ ├── App.vue # 主组件
|
|
165
|
-
│ ├── api/docs.js # 文档列表获取
|
|
166
114
|
│ ├── components/ # Vue 组件
|
|
167
115
|
│ ├── composables/ # 组合式函数
|
|
168
116
|
│ ├── config.js # 共享配置
|
package/bin/build.js
CHANGED
|
@@ -18,6 +18,13 @@ import { marked } from 'marked'
|
|
|
18
18
|
import hljs from 'highlight.js'
|
|
19
19
|
import GithubSlugger from 'github-slugger'
|
|
20
20
|
|
|
21
|
+
let katex
|
|
22
|
+
try {
|
|
23
|
+
katex = (await import('katex')).default
|
|
24
|
+
} catch {
|
|
25
|
+
katex = null
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
const __filename = fileURLToPath(import.meta.url)
|
|
22
29
|
const __dirname = dirname(__filename)
|
|
23
30
|
const pkgRoot = resolve(__dirname, '..')
|
|
@@ -189,10 +196,10 @@ async function renderMarkdownToHtml(markdown, currentDocKey, docsList) {
|
|
|
189
196
|
const slugger = new GithubSlugger()
|
|
190
197
|
const renderer = new marked.Renderer()
|
|
191
198
|
|
|
192
|
-
//
|
|
199
|
+
// 标题渲染(SSG 版本也加上 # 锚点链接)
|
|
193
200
|
renderer.heading = function(text, level) {
|
|
194
201
|
const id = slugger.slug(text)
|
|
195
|
-
return `<h${level} id="${id}">${text}</h${level}>\n`
|
|
202
|
+
return `<h${level} id="${id}"><a class="heading-anchor" href="#${id}" aria-hidden="true">#</a>${text}</h${level}>\n`
|
|
196
203
|
}
|
|
197
204
|
|
|
198
205
|
// 代码块渲染
|
|
@@ -253,6 +260,46 @@ async function renderMarkdownToHtml(markdown, currentDocKey, docsList) {
|
|
|
253
260
|
}
|
|
254
261
|
|
|
255
262
|
marked.setOptions({ renderer, breaks: true, gfm: true, headerIds: false, mangle: false })
|
|
263
|
+
|
|
264
|
+
// 注册 KaTeX 数学公式扩展
|
|
265
|
+
if (katex) {
|
|
266
|
+
marked.use({
|
|
267
|
+
extensions: [
|
|
268
|
+
{
|
|
269
|
+
name: 'mathBlock',
|
|
270
|
+
level: 'block',
|
|
271
|
+
start(src) {
|
|
272
|
+
const m = src.match(/(?:^|\n)\$\$/)
|
|
273
|
+
return m ? m.index + (m[0].startsWith('\n') ? 1 : 0) : -1
|
|
274
|
+
},
|
|
275
|
+
tokenizer(src) {
|
|
276
|
+
const match = src.match(/^\$\$\s*\n([\s\S]+?)\n\s*\$\$(?:\s*$|\n)/)
|
|
277
|
+
if (match) return { type: 'mathBlock', raw: match[0], text: match[1].trim() }
|
|
278
|
+
},
|
|
279
|
+
renderer(token) {
|
|
280
|
+
try {
|
|
281
|
+
return `<div class="math-block">${katex.renderToString(token.text, { throwOnError: false, displayMode: true })}</div>`
|
|
282
|
+
} catch { return `<pre class="math-error">${token.text}</pre>` }
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: 'mathInline',
|
|
287
|
+
level: 'inline',
|
|
288
|
+
start(src) { return src.indexOf('$') },
|
|
289
|
+
tokenizer(src) {
|
|
290
|
+
const match = src.match(/^\$(?!\$)((?:\\.|[^$\\])+)\$/)
|
|
291
|
+
if (match) return { type: 'mathInline', raw: match[0], text: match[1].trim() }
|
|
292
|
+
},
|
|
293
|
+
renderer(token) {
|
|
294
|
+
try {
|
|
295
|
+
return katex.renderToString(token.text, { throwOnError: false, displayMode: false })
|
|
296
|
+
} catch { return `<code class="math-error">${token.text}</code>` }
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
256
303
|
let html = marked.parse(content)
|
|
257
304
|
|
|
258
305
|
// frontmatter.title 覆盖 h1
|
|
@@ -370,8 +417,10 @@ function renderDocNav(flatDocs, currentIdx) {
|
|
|
370
417
|
|
|
371
418
|
// 生成完整的静态 HTML 页面
|
|
372
419
|
function generatePageHtml(options) {
|
|
373
|
-
const { title, siteTitle, contentHtml, sidebarHtml, docNavHtml, cssContent, themeColor, isWelcome } = options
|
|
420
|
+
const { title, siteTitle, contentHtml, sidebarHtml, docNavHtml, cssContent, themeColor, isWelcome, description, url } = options
|
|
374
421
|
const pageTitle = isWelcome ? siteTitle : `${title} - ${siteTitle}`
|
|
422
|
+
const metaDesc = description || title || siteTitle
|
|
423
|
+
const canonicalUrl = url || '/'
|
|
375
424
|
|
|
376
425
|
return `<!DOCTYPE html>
|
|
377
426
|
<html lang="zh-CN">
|
|
@@ -379,8 +428,19 @@ function generatePageHtml(options) {
|
|
|
379
428
|
<meta charset="UTF-8">
|
|
380
429
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
381
430
|
<title>${pageTitle}</title>
|
|
382
|
-
<meta name="description" content="${
|
|
431
|
+
<meta name="description" content="${metaDesc}">
|
|
383
432
|
<meta name="theme-color" content="${themeColor}">
|
|
433
|
+
<!-- Open Graph -->
|
|
434
|
+
<meta property="og:title" content="${pageTitle}">
|
|
435
|
+
<meta property="og:description" content="${metaDesc}">
|
|
436
|
+
<meta property="og:type" content="article">
|
|
437
|
+
<meta property="og:url" content="${canonicalUrl}">
|
|
438
|
+
<meta property="og:site_name" content="${siteTitle}">
|
|
439
|
+
<!-- Twitter Card -->
|
|
440
|
+
<meta name="twitter:card" content="summary">
|
|
441
|
+
<meta name="twitter:title" content="${pageTitle}">
|
|
442
|
+
<meta name="twitter:description" content="${metaDesc}">
|
|
443
|
+
<link rel="canonical" href="${canonicalUrl}">
|
|
384
444
|
<link rel="icon" type="image/svg+xml" href="/logo.svg">
|
|
385
445
|
<style>${cssContent}</style>
|
|
386
446
|
</head>
|
|
@@ -464,6 +524,13 @@ document.head.appendChild(s)})();
|
|
|
464
524
|
// 生成 SSG 专用 CSS(从 style.css 读取 + 补充 SSG 特有样式)
|
|
465
525
|
function getSsgCss(pkgRoot) {
|
|
466
526
|
let css = fs.readFileSync(resolve(pkgRoot, 'src/style.css'), 'utf-8')
|
|
527
|
+
// 尝试加载 KaTeX CSS
|
|
528
|
+
try {
|
|
529
|
+
const katexCssPath = resolve(pkgRoot, 'node_modules/katex/dist/katex.min.css')
|
|
530
|
+
if (fs.existsSync(katexCssPath)) {
|
|
531
|
+
css += '\n' + fs.readFileSync(katexCssPath, 'utf-8')
|
|
532
|
+
}
|
|
533
|
+
} catch { /* KaTeX CSS 不可用时忽略 */ }
|
|
467
534
|
// 追加 SSG 特有样式
|
|
468
535
|
css += `
|
|
469
536
|
/* SSG 特有样式 */
|
|
@@ -546,20 +613,28 @@ async function build() {
|
|
|
546
613
|
for (let i = 0; i < flatDocs.length; i++) {
|
|
547
614
|
const doc = flatDocs[i]
|
|
548
615
|
const markdown = fs.readFileSync(doc.path, 'utf-8')
|
|
549
|
-
const { html: contentHtml, title } = await renderMarkdownToHtml(markdown, doc.key, docsList)
|
|
616
|
+
const { html: contentHtml, title, frontmatter } = await renderMarkdownToHtml(markdown, doc.key, docsList)
|
|
550
617
|
const sidebarHtml = renderSidebarHtml(docsList, doc.key)
|
|
551
618
|
const docNavHtml = renderDocNav(flatDocs, i)
|
|
552
619
|
const hash = docHash(doc.key)
|
|
553
620
|
|
|
621
|
+
// 获取文件最后修改时间
|
|
622
|
+
const stat = fs.statSync(doc.path)
|
|
623
|
+
const mtime = stat.mtime
|
|
624
|
+
const lastModifiedStr = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, '0')}-${String(mtime.getDate()).padStart(2, '0')} ${String(mtime.getHours()).padStart(2, '0')}:${String(mtime.getMinutes()).padStart(2, '0')}`
|
|
625
|
+
const lastModifiedHtml = `<div class="doc-last-modified"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg><span>最后更新于 ${lastModifiedStr}</span></div>`
|
|
626
|
+
|
|
554
627
|
const pageHtml = generatePageHtml({
|
|
555
628
|
title: title || doc.label,
|
|
556
629
|
siteTitle: siteConfig.title,
|
|
557
630
|
contentHtml,
|
|
558
631
|
sidebarHtml,
|
|
559
|
-
docNavHtml,
|
|
632
|
+
docNavHtml: lastModifiedHtml + docNavHtml,
|
|
560
633
|
cssContent,
|
|
561
634
|
themeColor: siteConfig.themeColor,
|
|
562
|
-
isWelcome: false
|
|
635
|
+
isWelcome: false,
|
|
636
|
+
description: frontmatter.description || '',
|
|
637
|
+
url: `/${hash}.html`
|
|
563
638
|
})
|
|
564
639
|
|
|
565
640
|
fs.writeFileSync(resolve(outDir, `${hash}.html`), pageHtml, 'utf-8')
|
package/bin/md2ui.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createServer } from 'vite'
|
|
|
11
11
|
import { fileURLToPath } from 'url'
|
|
12
12
|
import { dirname, resolve } from 'path'
|
|
13
13
|
import fs from 'fs'
|
|
14
|
+
import crypto from 'crypto'
|
|
14
15
|
import { exec } from 'child_process'
|
|
15
16
|
import { pathToFileURL } from 'url'
|
|
16
17
|
|
|
@@ -136,11 +137,19 @@ function md2uiPlugin(siteConfig) {
|
|
|
136
137
|
configureServer(server) {
|
|
137
138
|
// API 中间件
|
|
138
139
|
server.middlewares.use((req, res, next) => {
|
|
139
|
-
// 文档列表 API
|
|
140
|
+
// 文档列表 API(带 ETag 支持,避免轮询时重复传输)
|
|
140
141
|
if (req.url === '/@user-docs-list') {
|
|
141
142
|
const docs = scanDocs(userDir, '', 0, siteConfig.folderExpanded)
|
|
143
|
+
const body = JSON.stringify(docs)
|
|
144
|
+
const etag = '"' + crypto.createHash('md5').update(body).digest('hex') + '"'
|
|
145
|
+
if (req.headers['if-none-match'] === etag) {
|
|
146
|
+
res.statusCode = 304
|
|
147
|
+
res.end()
|
|
148
|
+
return
|
|
149
|
+
}
|
|
142
150
|
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
143
|
-
res.
|
|
151
|
+
res.setHeader('ETag', etag)
|
|
152
|
+
res.end(body)
|
|
144
153
|
return
|
|
145
154
|
}
|
|
146
155
|
// 站点配置 API
|
|
@@ -149,12 +158,79 @@ function md2uiPlugin(siteConfig) {
|
|
|
149
158
|
res.end(JSON.stringify(siteConfig))
|
|
150
159
|
return
|
|
151
160
|
}
|
|
161
|
+
// 图片上传 API
|
|
162
|
+
if (req.url === '/@upload-image' && req.method === 'POST') {
|
|
163
|
+
const chunks = []
|
|
164
|
+
req.on('data', chunk => chunks.push(chunk))
|
|
165
|
+
req.on('end', () => {
|
|
166
|
+
try {
|
|
167
|
+
const body = Buffer.concat(chunks)
|
|
168
|
+
const docPath = decodeURIComponent(req.headers['x-doc-path'] || '')
|
|
169
|
+
const fileName = decodeURIComponent(req.headers['x-file-name'] || `img-${Date.now()}.png`)
|
|
170
|
+
|
|
171
|
+
if (!docPath) {
|
|
172
|
+
res.statusCode = 400; res.end('缺少文档路径'); return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const docDir = dirname(resolve(userDir, docPath))
|
|
176
|
+
if (!docDir.startsWith(userDir)) {
|
|
177
|
+
res.statusCode = 403; res.end('禁止访问'); return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const assetsDir = resolve(docDir, 'assets')
|
|
181
|
+
fs.mkdirSync(assetsDir, { recursive: true })
|
|
182
|
+
|
|
183
|
+
const ext = docPath.includes('.') ? `.${fileName.split('.').pop()}` : '.png'
|
|
184
|
+
const baseName = `img-${Date.now()}`
|
|
185
|
+
const targetName = `${baseName}${ext.startsWith('.') ? ext : '.' + ext}`
|
|
186
|
+
const targetPath = resolve(assetsDir, targetName)
|
|
187
|
+
|
|
188
|
+
fs.writeFileSync(targetPath, body)
|
|
189
|
+
|
|
190
|
+
// 返回相对于用户目录的路径(对路径各段做 URL 编码)
|
|
191
|
+
const docDirRel = dirname(docPath)
|
|
192
|
+
const encodedDir = docDirRel && docDirRel !== '.'
|
|
193
|
+
? docDirRel.split('/').map(encodeURIComponent).join('/')
|
|
194
|
+
: ''
|
|
195
|
+
const imageUrl = encodedDir
|
|
196
|
+
? `/@user-docs/${encodedDir}/assets/${targetName}`
|
|
197
|
+
: `/@user-docs/assets/${targetName}`
|
|
198
|
+
|
|
199
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
200
|
+
res.end(JSON.stringify({ url: imageUrl }))
|
|
201
|
+
} catch (e) {
|
|
202
|
+
res.statusCode = 500; res.end(e.message)
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
return
|
|
206
|
+
}
|
|
152
207
|
// 文档内容
|
|
153
208
|
if (req.url?.startsWith('/@user-docs/')) {
|
|
154
209
|
const filePath = resolve(userDir, decodeURIComponent(req.url.replace('/@user-docs/', '')))
|
|
155
210
|
if (fs.existsSync(filePath)) {
|
|
156
|
-
|
|
157
|
-
|
|
211
|
+
// 根据扩展名设置 Content-Type
|
|
212
|
+
const extName = filePath.split('.').pop().toLowerCase()
|
|
213
|
+
const mimeMap = {
|
|
214
|
+
md: 'text/plain; charset=utf-8',
|
|
215
|
+
png: 'image/png',
|
|
216
|
+
jpg: 'image/jpeg',
|
|
217
|
+
jpeg: 'image/jpeg',
|
|
218
|
+
gif: 'image/gif',
|
|
219
|
+
webp: 'image/webp',
|
|
220
|
+
svg: 'image/svg+xml',
|
|
221
|
+
bmp: 'image/bmp',
|
|
222
|
+
}
|
|
223
|
+
const contentType = mimeMap[extName] || 'application/octet-stream'
|
|
224
|
+
res.setHeader('Content-Type', contentType)
|
|
225
|
+
// 附带最后修改时间
|
|
226
|
+
const stat = fs.statSync(filePath)
|
|
227
|
+
res.setHeader('X-Last-Modified', stat.mtime.toISOString())
|
|
228
|
+
// 文本文件用 utf-8 读取,二进制文件直接读取
|
|
229
|
+
if (contentType.startsWith('text/')) {
|
|
230
|
+
res.end(fs.readFileSync(filePath, 'utf-8'))
|
|
231
|
+
} else {
|
|
232
|
+
res.end(fs.readFileSync(filePath))
|
|
233
|
+
}
|
|
158
234
|
return
|
|
159
235
|
}
|
|
160
236
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md2ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "将本地 Markdown 文档转换为美观的 HTML 页面",
|
|
6
6
|
"author": "",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/
|
|
10
|
+
"url": "https://github.com/xiaoyaodev/md2ui"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [
|
|
13
13
|
"markdown",
|
|
@@ -22,29 +22,43 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"dev": "vite",
|
|
24
24
|
"build": "vite build",
|
|
25
|
-
"preview": "vite preview"
|
|
26
|
-
"publish": "bash publish.sh"
|
|
25
|
+
"preview": "vite preview"
|
|
27
26
|
},
|
|
28
27
|
"files": [
|
|
29
28
|
"bin",
|
|
30
29
|
"src",
|
|
31
30
|
"public",
|
|
32
31
|
"index.html",
|
|
33
|
-
"vite.config.js"
|
|
32
|
+
"vite.config.js",
|
|
33
|
+
"vite-plugin-doc-api.js"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@tiptap/extension-code-block-lowlight": "^3.22.3",
|
|
37
|
+
"@tiptap/extension-image": "^3.22.3",
|
|
38
|
+
"@tiptap/extension-placeholder": "^3.22.3",
|
|
39
|
+
"@tiptap/extension-table": "^3.22.3",
|
|
40
|
+
"@tiptap/extension-table-cell": "^3.22.3",
|
|
41
|
+
"@tiptap/extension-table-header": "^3.22.3",
|
|
42
|
+
"@tiptap/extension-table-row": "^3.22.3",
|
|
43
|
+
"@tiptap/extension-task-item": "^3.22.3",
|
|
44
|
+
"@tiptap/extension-task-list": "^3.22.3",
|
|
45
|
+
"@tiptap/extension-underline": "^3.22.3",
|
|
46
|
+
"@tiptap/pm": "^3.22.4",
|
|
47
|
+
"@tiptap/starter-kit": "^3.22.3",
|
|
48
|
+
"@tiptap/vue-3": "^3.22.3",
|
|
36
49
|
"@vitejs/plugin-vue": "^5.0.0",
|
|
37
50
|
"docx": "^9.6.1",
|
|
38
|
-
"file-saver": "^2.0.5",
|
|
39
|
-
"flexsearch": "^0.8.212",
|
|
40
51
|
"github-slugger": "^2.0.0",
|
|
41
52
|
"highlight.js": "^11.11.1",
|
|
42
|
-
"
|
|
53
|
+
"highlightjs-line-numbers.js": "^2.9.1",
|
|
54
|
+
"katex": "^0.16.45",
|
|
43
55
|
"lucide-vue-next": "^0.556.0",
|
|
44
56
|
"marked": "^11.1.1",
|
|
45
57
|
"mermaid": "^10.6.1",
|
|
46
58
|
"minisearch": "^7.2.0",
|
|
59
|
+
"tiptap-markdown": "^0.9.0",
|
|
47
60
|
"vite": "^5.0.0",
|
|
48
|
-
"vue": "^3.4.0"
|
|
61
|
+
"vue": "^3.4.0",
|
|
62
|
+
"vuedraggable": "^4.1.0"
|
|
49
63
|
}
|
|
50
64
|
}
|
|
@@ -1,51 +1,71 @@
|
|
|
1
1
|
# 快速开始
|
|
2
2
|
|
|
3
|
-
欢迎使用 md2ui
|
|
3
|
+
欢迎使用 md2ui,一个轻量级的 Markdown 文档渲染工具。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 安装
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
```bash
|
|
8
|
+
# 全局安装
|
|
9
|
+
pnpm add -g md2ui
|
|
10
|
+
|
|
11
|
+
# 或使用 npm
|
|
12
|
+
npm install -g md2ui
|
|
13
|
+
```
|
|
9
14
|
|
|
10
|
-
##
|
|
15
|
+
## 启动预览
|
|
16
|
+
|
|
17
|
+
在包含 `.md` 文件的目录下运行:
|
|
11
18
|
|
|
12
19
|
```bash
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
cd /path/to/your/docs
|
|
21
|
+
md2ui
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
访问 http://localhost:3000 即可查看文档。
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
pnpm install
|
|
26
|
+
## 指定端口
|
|
19
27
|
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
```bash
|
|
29
|
+
md2ui -p 8080
|
|
22
30
|
```
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
## 静态构建
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
```bash
|
|
35
|
+
md2ui build
|
|
36
|
+
```
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
生成的静态文件在 `dist/` 目录下,可直接部署到任意静态服务器。
|
|
39
|
+
|
|
40
|
+
## 文档组织
|
|
29
41
|
|
|
30
42
|
```
|
|
31
|
-
|
|
43
|
+
your-docs/
|
|
44
|
+
├── README.md # 首页内容
|
|
32
45
|
├── 00-快速开始.md
|
|
33
|
-
├── 01
|
|
34
|
-
└── 02
|
|
35
|
-
├── 01
|
|
36
|
-
└── 02
|
|
46
|
+
├── 01-功能特性.md
|
|
47
|
+
└── 02-进阶指南/
|
|
48
|
+
├── 01-目录结构.md
|
|
49
|
+
└── 02-自定义配置.md
|
|
37
50
|
```
|
|
38
51
|
|
|
39
|
-
|
|
52
|
+
- 使用 `序号-名称.md` 格式控制排序,如 `01-快速开始.md`
|
|
53
|
+
- 文件夹也支持序号前缀,如 `02-进阶指南/`
|
|
54
|
+
- 序号越小越靠前
|
|
40
55
|
|
|
41
|
-
##
|
|
56
|
+
## 自定义配置
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
# 构建生产版本
|
|
45
|
-
pnpm build
|
|
58
|
+
在文档目录下创建 `md2ui.config.js`:
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
```js
|
|
61
|
+
export default {
|
|
62
|
+
title: '我的文档站',
|
|
63
|
+
port: 8080,
|
|
64
|
+
folderExpanded: true,
|
|
65
|
+
themeColor: '#3eaf7c',
|
|
66
|
+
github: 'https://github.com/your/repo',
|
|
67
|
+
footer: 'Copyright © 2025'
|
|
68
|
+
}
|
|
49
69
|
```
|
|
50
70
|
|
|
51
|
-
|
|
71
|
+
更多配置项请参考 [自定义](10-%E9%83%A8%E7%BD%B2%E4%B8%8E%E9%85%8D%E7%BD%AE/03-%E8%87%AA%E5%AE%9A%E4%B9%89%E9%85%8D%E7%BD%AE.md)
|
|
@@ -1,57 +1,72 @@
|
|
|
1
1
|
# 功能特性
|
|
2
2
|
|
|
3
|
-
md2ui
|
|
3
|
+
md2ui 提供了丰富的功能,覆盖文档渲染、编辑、搜索、导航、移动端、导出、部署等完整链路。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Markdown 渲染
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- GFM 语法支持(表格、任务列表、删除线等)
|
|
8
|
+
- 代码语法高亮(基于 highlight.js,支持行号、自动换行、高亮开关)
|
|
9
|
+
- Mermaid 图表(流程图、序列图、甘特图、饼图等)
|
|
10
|
+
- Frontmatter 解析(title / description / order / hidden)
|
|
11
|
+
- 阅读时间估算(中文 400 字/分钟,英文 200 词/分钟)
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
- 中间:文档内容区,渲染 Markdown
|
|
11
|
-
- 右侧:文档大纲,快速跳转章节
|
|
13
|
+
## 导航与布局
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
- 三栏布局(左侧导航 / 中间内容 / 右侧大纲,可拖拽调整宽度)
|
|
16
|
+
- 自动目录树(递归扫描文件夹,按序号排序,支持多级嵌套)
|
|
17
|
+
- 文档大纲 TOC(自动提取 h1\~h6,高亮当前位置,点击跳转)
|
|
18
|
+
- 上下篇导航(文档底部自动生成前后文档链接)
|
|
19
|
+
- 导航过滤(侧边栏关键词过滤文档树)
|
|
20
|
+
- 侧边栏折叠/展开(收起导航栏、全部展开/收起文件夹)
|
|
21
|
+
- 站内链接改写(.md 文件间相对链接自动转为 SPA 路由)
|
|
14
22
|
|
|
15
|
-
##
|
|
23
|
+
## 全文搜索
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
- 基于 MiniSearch 的快速全文检索
|
|
26
|
+
- 中文 bigram 分词、模糊匹配、前缀搜索
|
|
27
|
+
- Ctrl+K 快捷键唤起搜索框
|
|
28
|
+
- 懒加载索引(首次打开搜索时构建)
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
## 编辑功能
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
- 查看/编辑模式一键切换
|
|
33
|
+
- 基于 Tiptap 的富文本编辑器
|
|
34
|
+
- 支持加粗、斜体、下划线、删除线、行内代码
|
|
35
|
+
- H1\~H4 标题、无序/有序/任务列表
|
|
36
|
+
- 代码块编辑(语言标签 + 复制按钮)
|
|
37
|
+
- 表格编辑(可调整大小,气泡菜单操作行列)
|
|
38
|
+
- Mermaid 编辑(预览/编辑双模式,实时渲染)
|
|
39
|
+
- 图片插入、撤销/重做
|
|
40
|
+
- 自动保存(1 秒防抖,组件卸载时立即保存)
|
|
27
41
|
|
|
28
|
-
|
|
29
|
-
# Python 代码示例
|
|
30
|
-
def fibonacci(n):
|
|
31
|
-
if n <= 1:
|
|
32
|
-
return n
|
|
33
|
-
return fibonacci(n-1) + fibonacci(n-2)
|
|
34
|
-
```
|
|
42
|
+
## 阅读体验
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
- 阅读进度条(显示滚动百分比)
|
|
45
|
+
- 返回顶部按钮
|
|
46
|
+
- 图片点击放大(支持多图切换)
|
|
47
|
+
- 表格增强(固定宽度/横向滚动、固定/自适应高度、全屏查看)
|
|
48
|
+
- 代码一键复制
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|------|------|------|
|
|
40
|
-
| 代码高亮 | 支持多种编程语言 | 已完成 |
|
|
41
|
-
| Mermaid 图表 | 流程图、时序图等 | 已完成 |
|
|
42
|
-
| 数学公式 | LaTeX 语法支持 | 计划中 |
|
|
50
|
+
## 移动端适配
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
- 响应式布局(768px 断点自动切换)
|
|
53
|
+
- 抽屉式导航(带遮罩)
|
|
54
|
+
- 移动端 TOC(底部浮动目录面板)
|
|
55
|
+
- 独立移动端顶栏
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
- [x] 代码语法高亮
|
|
48
|
-
- [x] Mermaid 图表支持
|
|
49
|
-
- [ ] 全文搜索
|
|
50
|
-
- [ ] 多主题切换
|
|
57
|
+
## 文档管理
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
- 新建文档/目录(侧边栏菜单创建)
|
|
60
|
+
- 删除文档/目录(带确认弹窗)
|
|
61
|
+
- 文件热更新(轮询监听,ETag 机制自动刷新)
|
|
53
62
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
-
|
|
57
|
-
|
|
63
|
+
## 导出
|
|
64
|
+
|
|
65
|
+
- 导出 Word(.docx 格式,支持标题、段落、表格、列表、代码块、Mermaid 图表、图片、引用块、超链接)
|
|
66
|
+
|
|
67
|
+
## 部署与 CLI
|
|
68
|
+
|
|
69
|
+
- `md2ui` 命令一键启动开发服务器
|
|
70
|
+
- `md2ui build` 生成纯静态 HTML(SSG)
|
|
71
|
+
- 自定义配置(标题、端口、主题色、GitHub 链接、页脚等)
|
|
72
|
+
- SPA 路由(基于 hash,支持浏览器前进/后退)
|