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.
Files changed (74) hide show
  1. package/README.md +3 -55
  2. package/bin/build.js +82 -7
  3. package/bin/md2ui.js +80 -4
  4. package/package.json +23 -9
  5. package/public/docs/00-/345/277/253/351/200/237/345/274/200/345/247/213.md +48 -28
  6. package/public/docs/01-/345/212/237/350/203/275/347/211/271/346/200/247.md +55 -40
  7. 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
  8. package/public/docs/02-Markdown/346/270/262/346/237/223/01-/344/273/243/347/240/201/345/235/227.md +91 -0
  9. package/public/docs/02-Markdown/346/270/262/346/237/223/02-/350/241/250/346/240/274.md +187 -0
  10. package/public/docs/02-Markdown/346/270/262/346/237/223/03-Mermaid/345/233/276/350/241/250.md +101 -0
  11. package/public/docs/02-Markdown/346/270/262/346/237/223/04-Frontmatter.md +32 -0
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/assets/img-1777261394722.png +0 -0
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. package/src/App.vue +130 -6
  35. package/src/components/AppSidebar.vue +181 -21
  36. package/src/components/CodeBlockNodeView.vue +72 -0
  37. package/src/components/DocContent.vue +25 -14
  38. package/src/components/EditorContent.vue +257 -0
  39. package/src/components/EditorToolbar.vue +264 -0
  40. package/src/components/ImageZoom.vue +199 -2
  41. package/src/components/MathBlockNodeView.vue +160 -0
  42. package/src/components/MathInlineNodeView.vue +145 -0
  43. package/src/components/MermaidNodeView.vue +149 -0
  44. package/src/components/TableBubbleMenu.vue +177 -0
  45. package/src/components/TableOfContents.vue +138 -32
  46. package/src/components/TopBar.vue +69 -4
  47. package/src/components/TreeNode.vue +232 -39
  48. package/src/components/WelcomePage.vue +2 -2
  49. package/src/composables/useDocHash.js +9 -1
  50. package/src/composables/useDocManager.js +325 -68
  51. package/src/composables/useDocTree.js +56 -1
  52. package/src/composables/useExportPdf.js +102 -0
  53. package/src/composables/useExportWord.js +73 -10
  54. package/src/composables/useFileWatcher.js +45 -0
  55. package/src/composables/useFrontmatter.js +2 -2
  56. package/src/composables/useMarkdown.js +529 -42
  57. package/src/composables/useScroll.js +47 -5
  58. package/src/config.js +1 -1
  59. package/src/extensions/CodeBlockCustom.js +113 -0
  60. package/src/extensions/MathBlock.js +107 -0
  61. package/src/extensions/MathInline.js +100 -0
  62. package/src/extensions/MermaidBlock.js +73 -0
  63. package/src/extensions/TableControls.js +670 -0
  64. package/src/services/DocService.js +184 -0
  65. package/src/style.css +2194 -39
  66. package/vite-plugin-doc-api.js +368 -0
  67. package/vite.config.js +2 -1
  68. package/public/docs/02-Mermaid/345/233/276/350/241/250.md +0 -102
  69. 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
  70. 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
  71. 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
  72. package/public/docs/04-API/345/217/202/350/200/203/01-/347/273/204/344/273/266API.md +0 -80
  73. package/public/docs/04-API/345/217/202/350/200/203/02-Composables.md +0 -92
  74. package/src/api/docs.js +0 -106
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # md2ui
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/md2ui)](https://www.npmjs.com/package/md2ui)
4
- [![GitHub](https://img.shields.io/badge/GitHub-devsneed%2Fmd2ui-blue?logo=github)](https://github.com/devsneed/md2ui)
4
+ [![GitHub](https://img.shields.io/badge/GitHub-xiaoyaodev%2Fmd2ui-blue?logo=github)](https://github.com/xiaoyaodev/md2ui)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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
- ![全文搜索](screenshots/task1-search.png)
26
-
27
- ### 代码高亮
28
-
29
- ![代码高亮 - 语言标签与复制按钮](screenshots/task2-code-highlight-top.png)
30
-
31
- ![代码高亮 - 渲染效果](screenshots/task2-code-highlight-view.png)
32
-
33
- ### 暗色模式
34
-
35
- ![暗色模式 - 欢迎页](screenshots/task3-dark-mode-welcome.png)
36
-
37
- ![暗色模式 - 文档内容](screenshots/task3-dark-mode-content.png)
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
- ![移动端 - 文档内容](screenshots/task4-mobile-content.png)
48
-
49
- ### 上下篇导航
50
-
51
- ![上下篇导航](screenshots/task5-prev-next-nav.png)
52
-
53
- ### 阅读时间
54
-
55
- ![阅读时间](screenshots/task7-reading-time.png)
56
-
57
- ### Frontmatter 支持
58
-
59
- ![Frontmatter](screenshots/task8-frontmatter.png)
60
-
61
- ### 导航过滤
62
-
63
- ![导航过滤](screenshots/task9-nav-filter.png)
64
-
65
- ### SSG 静态构建
66
-
67
- ![SSG - 首页](screenshots/ssg-index.png)
68
-
69
- ![SSG - 目录展开](screenshots/ssg-folder-open.png)
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/your/repo',
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/devsneed/md2ui.git
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="${title}">
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.end(JSON.stringify(docs))
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
- res.setHeader('Content-Type', 'text/plain; charset=utf-8')
157
- res.end(fs.readFileSync(filePath, 'utf-8'))
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.16",
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/your-username/md2ui"
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
- "jsdom": "^28.1.0",
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
- - Node.js 18+
8
- - pnpm 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
- git clone https://github.com/your-username/md2ui.git
15
- cd md2ui
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
- pnpm dev
28
+ ```bash
29
+ md2ui -p 8080
22
30
  ```
23
31
 
24
- 启动后访问 http://localhost:5173 即可看到文档界面。
32
+ ## 静态构建
25
33
 
26
- ## 添加你的文档
34
+ ```bash
35
+ md2ui build
36
+ ```
27
37
 
28
- Markdown 文件放入 `public/docs/` 目录:
38
+ 生成的静态文件在 `dist/` 目录下,可直接部署到任意静态服务器。
39
+
40
+ ## 文档组织
29
41
 
30
42
  ```
31
- public/docs/
43
+ your-docs/
44
+ ├── README.md # 首页内容
32
45
  ├── 00-快速开始.md
33
- ├── 01-使用指南.md
34
- └── 02-进阶配置/
35
- ├── 01-主题定制.md
36
- └── 02-部署方案.md
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
- ```bash
44
- # 构建生产版本
45
- pnpm build
58
+ 在文档目录下创建 `md2ui.config.js`:
46
59
 
47
- # 本地预览
48
- pnpm preview
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
- 构建产物在 `dist/` 目录,可部署到任意静态服务器。
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
- ## Markdown 增强
23
+ ## 全文搜索
16
24
 
17
- 支持 GitHub Flavored Markdown (GFM) 语法:
25
+ - 基于 MiniSearch 的快速全文检索
26
+ - 中文 bigram 分词、模糊匹配、前缀搜索
27
+ - Ctrl+K 快捷键唤起搜索框
28
+ - 懒加载索引(首次打开搜索时构建)
18
29
 
19
- ### 代码高亮
30
+ ## 编辑功能
20
31
 
21
- ```javascript
22
- // 支持多种语言的语法高亮
23
- function greet(name) {
24
- return `Hello, ${name}!`
25
- }
26
- ```
32
+ - 查看/编辑模式一键切换
33
+ - 基于 Tiptap 的富文本编辑器
34
+ - 支持加粗、斜体、下划线、删除线、行内代码
35
+ - H1\~H4 标题、无序/有序/任务列表
36
+ - 代码块编辑(语言标签 + 复制按钮)
37
+ - 表格编辑(可调整大小,气泡菜单操作行列)
38
+ - Mermaid 编辑(预览/编辑双模式,实时渲染)
39
+ - 图片插入、撤销/重做
40
+ - 自动保存(1 秒防抖,组件卸载时立即保存)
27
41
 
28
- ```python
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
- - [x] 基础 Markdown 渲染
47
- - [x] 代码语法高亮
48
- - [x] Mermaid 图表支持
49
- - [ ] 全文搜索
50
- - [ ] 多主题切换
57
+ ## 文档管理
51
58
 
52
- ## 交互体验
59
+ - 新建文档/目录(侧边栏菜单创建)
60
+ - 删除文档/目录(带确认弹窗)
61
+ - 文件热更新(轮询监听,ETag 机制自动刷新)
53
62
 
54
- - 阅读进度显示
55
- - 返回顶部按钮
56
- - 图片点击放大
57
- - URL 路由支持(可分享链接)
63
+ ## 导出
64
+
65
+ - 导出 Word(.docx 格式,支持标题、段落、表格、列表、代码块、Mermaid 图表、图片、引用块、超链接)
66
+
67
+ ## 部署与 CLI
68
+
69
+ - `md2ui` 命令一键启动开发服务器
70
+ - `md2ui build` 生成纯静态 HTML(SSG)
71
+ - 自定义配置(标题、端口、主题色、GitHub 链接、页脚等)
72
+ - SPA 路由(基于 hash,支持浏览器前进/后退)