md2ui 1.0.18 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +51 -58
  2. package/bin/build.js +95 -9
  3. package/bin/md2ui.js +102 -13
  4. package/package.json +24 -10
  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 +88 -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/02-Markdown/346/270/262/346/237/223/06-Mermaid/345/244/215/346/235/202/345/233/276/350/241/250/346/265/213/350/257/225.md +1376 -0
  14. package/public/docs/02-Markdown/346/270/262/346/237/223/assets/img-1777383093712.png +0 -0
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/05-/345/244/247/347/272/262/345/216/213/345/212/233/346/265/213/350/257/225.md +340 -0
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/assets/img-1777261394722.png +0 -0
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. package/src/App.vue +111 -12
  38. package/src/components/AppSidebar.vue +181 -21
  39. package/src/components/CodeBlockNodeView.vue +72 -0
  40. package/src/components/DocContent.vue +25 -14
  41. package/src/components/EditorContent.vue +257 -0
  42. package/src/components/EditorToolbar.vue +264 -0
  43. package/src/components/ImageZoom.vue +88 -5
  44. package/src/components/MathBlockNodeView.vue +160 -0
  45. package/src/components/MathInlineNodeView.vue +145 -0
  46. package/src/components/MermaidNodeView.vue +157 -0
  47. package/src/components/MobileSearch.vue +97 -0
  48. package/src/components/TableOfContents.vue +174 -32
  49. package/src/components/TopBar.vue +69 -4
  50. package/src/components/TreeNode.vue +232 -39
  51. package/src/components/WelcomePage.vue +2 -2
  52. package/src/composables/useDocHash.js +9 -1
  53. package/src/composables/useDocManager.js +452 -105
  54. package/src/composables/useDocTree.js +33 -2
  55. package/src/composables/useExportWord.js +73 -10
  56. package/src/composables/useFileWatcher.js +45 -0
  57. package/src/composables/useFrontmatter.js +2 -2
  58. package/src/composables/useMarkdown.js +450 -52
  59. package/src/composables/useMermaidCache.js +15 -0
  60. package/src/composables/useScroll.js +354 -27
  61. package/src/composables/useSearch.js +12 -11
  62. package/src/config.js +1 -4
  63. package/src/extensions/CodeBlockCustom.js +113 -0
  64. package/src/extensions/MathBlock.js +107 -0
  65. package/src/extensions/MathInline.js +100 -0
  66. package/src/extensions/MermaidBlock.js +73 -0
  67. package/src/extensions/TableControls.js +670 -0
  68. package/src/services/DocService.js +168 -0
  69. package/src/style.css +2416 -36
  70. package/src/utils/imageConverter.js +129 -0
  71. package/vite-plugin-doc-api.js +369 -0
  72. package/vite.config.js +7 -2
  73. package/public/docs/02-Mermaid/345/233/276/350/241/250.md +0 -102
  74. 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
  75. 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
  76. 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
  77. package/public/docs/04-API/345/217/202/350/200/203/01-/347/273/204/344/273/266API.md +0 -80
  78. package/public/docs/04-API/345/217/202/350/200/203/02-Composables.md +0 -92
  79. package/src/api/docs.js +0 -106
  80. package/src/components/SearchPanel.vue +0 -90
package/README.md CHANGED
@@ -1,81 +1,75 @@
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
- 一个轻量级的 Markdown 文档渲染工具,将本地 `.md` 文件转换为美观的 HTML 页面。支持实时预览和静态站点生成(SSG)两种模式。
7
+ 轻量级 Markdown 文档站点工具,一行命令将本地 `.md` 文件转换为可预览、可编辑、可搜索的文档站点。
8
8
 
9
- ## 功能特性
9
+ ## 为什么需要 md2ui?
10
10
 
11
- - 自动目录树 - 扫描文档文件夹,自动生成多级导航,支持搜索过滤
12
- - 三栏布局 - 左侧导航 / 中间内容 / 右侧大纲,可拖拽调整宽度
13
- - Markdown 增强 - GFM 语法、代码高亮、Mermaid 图表、Frontmatter
14
- - 全文搜索 - 基于 MiniSearch 的快速全文检索
15
- - 暗色模式 - 一键切换亮色/暗色主题
16
- - 移动端适配 - 响应式布局,抽屉式导航和目录
17
- - 阅读体验 - 阅读进度条、预计阅读时间、上下篇导航
18
- - SSG 构建 - `md2ui build` 生成纯静态 HTML,可直接部署
19
- - 自定义配置 - 站点标题、主题色、GitHub 链接、页脚等
20
-
21
- ## 效果预览
22
-
23
- ### 全文搜索
24
-
25
- ![全文搜索](screenshots/task1-search.png)
26
-
27
- ### 代码高亮
28
-
29
- ![代码高亮 - 语言标签与复制按钮](screenshots/task2-code-highlight-top.png)
11
+ AI 编程时代,Cursor、Copilot、Kiro 等工具正在改变我们的开发方式。与 AI 协作的过程中,会产生大量的 Markdown 文档 —— 需求文档、设计方案、API 文档、会议纪要、技术调研……
30
12
 
31
- ![代码高亮 - 渲染效果](screenshots/task2-code-highlight-view.png)
13
+ 这些 `.md` 文件散落在项目各处,带来了一系列痛点:
32
14
 
33
- ### 暗色模式
15
+ - 文件越来越多,找一篇文档要翻半天
16
+ - IDE 的 Markdown 预览体验一般,表格、流程图、数学公式渲染不理想
17
+ - 想快速改几个字,还得切回编辑器找到对应文件
18
+ - 文档之间缺乏导航关系,无法形成知识体系
19
+ - 想分享给团队成员,还得额外搭建文档站
34
20
 
35
- ![暗色模式 - 欢迎页](screenshots/task3-dark-mode-welcome.png)
21
+ md2ui 就是为了解决这些问题而生的。把它指向你的文档目录,立刻获得一个功能完整的文档站点。
36
22
 
37
- ![暗色模式 - 文档内容](screenshots/task3-dark-mode-content.png)
23
+ ## 核心能力
38
24
 
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)
25
+ - 零配置启动 - `cd docs && md2ui`,开箱即用
26
+ - 实时预览 - 文件变更自动刷新,所见即所得
27
+ - 在线编辑 - 内置富文本编辑器,直接在浏览器中修改并保存到本地文件
28
+ - 全文搜索 - 基于 MiniSearch,毫秒级检索所有文档内容
29
+ - 自动导航 - 扫描目录结构自动生成多级目录树,支持拖拽排序
30
+ - 三栏布局 - 左侧导航 / 中间内容 / 右侧大纲,宽度可拖拽调整
31
+ - Markdown 增强 - GFM 语法、代码高亮、Mermaid 图表、数学公式、Frontmatter
32
+ - 移动端适配 - 响应式布局,手机上也能舒适阅读
33
+ - 阅读体验 - 进度条、预计阅读时间、上下篇导航、图片放大
34
+ - SSG 构建 - `md2ui build` 生成纯静态站点,可直接部署
35
+ - 自定义配置 - 站点标题、主题色、GitHub 链接、页脚等
48
36
 
49
- ### 上下篇导航
37
+ ## 界面预览
38
+ 预览模式
39
+ ![预览模式](imgs/1.png)
40
+ 编辑模式
50
41
 
51
- ![上下篇导航](screenshots/task5-prev-next-nav.png)
42
+ ![编辑模式](imgs/2.png)
52
43
 
53
- ### 阅读时间
44
+ ## 典型使用场景
54
45
 
55
- ![阅读时间](screenshots/task7-reading-time.png)
46
+ **AI 辅助开发的文档管理**
56
47
 
57
- ### Frontmatter 支持
48
+ AI 生成的需求文档、设计方案、技术调研统一放到一个目录,md2ui 提供即时预览和编辑,让文档真正流动起来。
58
49
 
59
- ![Frontmatter](screenshots/task8-frontmatter.png)
50
+ **个人知识库**
60
51
 
61
- ### 导航过滤
52
+ 日常笔记、学习记录、读书摘要,用 Markdown 写完丢进文件夹,md2ui 自动组织成可浏览的知识站点。
62
53
 
63
- ![导航过滤](screenshots/task9-nav-filter.png)
54
+ **项目文档站**
64
55
 
65
- ### SSG 静态构建
56
+ API 文档、部署指南、开发规范,`md2ui build` 一键构建为静态站点,部署到任意服务器。
66
57
 
67
- ![SSG - 首页](screenshots/ssg-index.png)
58
+ **团队协作文档**
68
59
 
69
- ![SSG - 目录展开](screenshots/ssg-folder-open.png)
60
+ 配合 Git 管理文档版本,本地用 md2ui 预览和编辑,提交后自动构建部署。
70
61
 
71
- ## 安装使用
62
+ ## 快速开始
72
63
 
73
- ### 全局安装
64
+ ### 安装
74
65
 
75
66
  ```bash
76
- pnpm add -g md2ui
77
- # 或
67
+ # 需要提前安装nodejs运行环境
78
68
  npm install -g md2ui
69
+
70
+
71
+ # 免安装 一次性运行
72
+ npx md2ui
79
73
  ```
80
74
 
81
75
  ### 实时预览
@@ -87,21 +81,19 @@ cd /path/to/your/docs
87
81
  md2ui
88
82
  ```
89
83
 
90
- 指定端口:
84
+ 访问 http://localhost:3000 即可查看文档。支持 `-p` 参数指定端口:
91
85
 
92
86
  ```bash
93
87
  md2ui -p 8080
94
88
  ```
95
89
 
96
- 访问 http://localhost:3000 查看文档(默认端口 3000)。
97
-
98
90
  ### 静态构建
99
91
 
100
92
  ```bash
101
93
  md2ui build
102
94
  ```
103
95
 
104
- 生成的静态文件在 `dist/` 目录下,可直接部署到任意静态服务器。
96
+ 生成的静态文件在 `dist/` 目录下,可直接部署到 Nginx、GitHub Pages、Vercel 等任意静态托管服务。
105
97
 
106
98
  ## 文档组织
107
99
 
@@ -116,8 +108,8 @@ your-docs/
116
108
  ```
117
109
 
118
110
  - 使用 `序号-名称.md` 格式控制排序,如 `01-快速开始.md`
119
- - 文件夹也支持序号前缀,如 `02-进阶指南/`
120
- - 序号越小越靠前
111
+ - 文件夹同样支持序号前缀,如 `02-进阶指南/`
112
+ - 支持任意层级嵌套
121
113
 
122
114
  ## 自定义配置
123
115
 
@@ -147,7 +139,7 @@ export default {
147
139
  ## 开发
148
140
 
149
141
  ```bash
150
- git clone https://github.com/devsneed/md2ui.git
142
+ git clone https://github.com/xiaoyaodev/md2ui.git
151
143
  cd md2ui
152
144
  pnpm install
153
145
  pnpm dev
@@ -162,9 +154,10 @@ md2ui/
162
154
  │ └── build.js # SSG 静态构建
163
155
  ├── src/
164
156
  │ ├── App.vue # 主组件
165
- │ ├── api/docs.js # 文档列表获取
166
157
  │ ├── components/ # Vue 组件
167
158
  │ ├── composables/ # 组合式函数
159
+ │ ├── extensions/ # Tiptap 编辑器扩展
160
+ │ ├── services/ # 文档服务
168
161
  │ ├── config.js # 共享配置
169
162
  │ └── style.css # 全局样式
170
163
  ├── public/docs/ # 示例文档
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 特有样式 */
@@ -507,8 +574,19 @@ function getSsgCss(pkgRoot) {
507
574
 
508
575
  // ===== 主构建流程 =====
509
576
  async function build() {
577
+ // 解析位置参数:md2ui build [dir]
578
+ const args = process.argv.slice(3)
579
+ let targetDir = null
580
+ for (const arg of args) {
581
+ if (!arg.startsWith('-')) {
582
+ targetDir = arg
583
+ break
584
+ }
585
+ }
586
+ const scanDir = targetDir ? resolve(userDir, targetDir) : userDir
587
+
510
588
  console.log('\n md2ui build - 静态站点生成\n')
511
- console.log(` 扫描目录: ${userDir}\n`)
589
+ console.log(` 扫描目录: ${scanDir}\n`)
512
590
 
513
591
  // 加载配置
514
592
  const userConfig = await loadUserConfig()
@@ -516,7 +594,7 @@ async function build() {
516
594
  const outDir = resolve(userDir, siteConfig.outDir || 'dist')
517
595
 
518
596
  // 扫描文档
519
- const docsList = scanDocs(userDir, '', 0, siteConfig.folderExpanded)
597
+ const docsList = scanDocs(scanDir, '', 0, siteConfig.folderExpanded)
520
598
  const flatDocs = flattenDocs(docsList)
521
599
 
522
600
  if (flatDocs.length === 0) {
@@ -546,20 +624,28 @@ async function build() {
546
624
  for (let i = 0; i < flatDocs.length; i++) {
547
625
  const doc = flatDocs[i]
548
626
  const markdown = fs.readFileSync(doc.path, 'utf-8')
549
- const { html: contentHtml, title } = await renderMarkdownToHtml(markdown, doc.key, docsList)
627
+ const { html: contentHtml, title, frontmatter } = await renderMarkdownToHtml(markdown, doc.key, docsList)
550
628
  const sidebarHtml = renderSidebarHtml(docsList, doc.key)
551
629
  const docNavHtml = renderDocNav(flatDocs, i)
552
630
  const hash = docHash(doc.key)
553
631
 
632
+ // 获取文件最后修改时间
633
+ const stat = fs.statSync(doc.path)
634
+ const mtime = stat.mtime
635
+ 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')}`
636
+ 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>`
637
+
554
638
  const pageHtml = generatePageHtml({
555
639
  title: title || doc.label,
556
640
  siteTitle: siteConfig.title,
557
641
  contentHtml,
558
642
  sidebarHtml,
559
- docNavHtml,
643
+ docNavHtml: lastModifiedHtml + docNavHtml,
560
644
  cssContent,
561
645
  themeColor: siteConfig.themeColor,
562
- isWelcome: false
646
+ isWelcome: false,
647
+ description: frontmatter.description || '',
648
+ url: `/${hash}.html`
563
649
  })
564
650
 
565
651
  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
 
@@ -62,11 +63,14 @@ async function loadUserConfig() {
62
63
  // 解析命令行参数
63
64
  function parseArgs() {
64
65
  const args = process.argv.slice(2)
65
- const result = {}
66
+ const result = { dir: null }
66
67
  for (let i = 0; i < args.length; i++) {
67
68
  if (args[i] === '-p' || args[i] === '--port') {
68
69
  result.port = parseInt(args[i + 1]) || undefined
69
70
  i++
71
+ } else if (!args[i].startsWith('-')) {
72
+ // 位置参数作为扫描目录
73
+ result.dir = args[i]
70
74
  }
71
75
  }
72
76
  return result
@@ -130,17 +134,25 @@ function hasMdFiles(dir) {
130
134
  }
131
135
 
132
136
  // Vite 插件:提供用户文档 API + 配置 API + 热更新
133
- function md2uiPlugin(siteConfig) {
137
+ function md2uiPlugin(siteConfig, docsRoot) {
134
138
  return {
135
139
  name: 'md2ui-server',
136
140
  configureServer(server) {
137
141
  // API 中间件
138
142
  server.middlewares.use((req, res, next) => {
139
- // 文档列表 API
143
+ // 文档列表 API(带 ETag 支持,避免轮询时重复传输)
140
144
  if (req.url === '/@user-docs-list') {
141
- const docs = scanDocs(userDir, '', 0, siteConfig.folderExpanded)
145
+ const docs = scanDocs(docsRoot, '', 0, siteConfig.folderExpanded)
146
+ const body = JSON.stringify(docs)
147
+ const etag = '"' + crypto.createHash('md5').update(body).digest('hex') + '"'
148
+ if (req.headers['if-none-match'] === etag) {
149
+ res.statusCode = 304
150
+ res.end()
151
+ return
152
+ }
142
153
  res.setHeader('Content-Type', 'application/json; charset=utf-8')
143
- res.end(JSON.stringify(docs))
154
+ res.setHeader('ETag', etag)
155
+ res.end(body)
144
156
  return
145
157
  }
146
158
  // 站点配置 API
@@ -149,12 +161,79 @@ function md2uiPlugin(siteConfig) {
149
161
  res.end(JSON.stringify(siteConfig))
150
162
  return
151
163
  }
164
+ // 图片上传 API
165
+ if (req.url === '/@upload-image' && req.method === 'POST') {
166
+ const chunks = []
167
+ req.on('data', chunk => chunks.push(chunk))
168
+ req.on('end', () => {
169
+ try {
170
+ const body = Buffer.concat(chunks)
171
+ const docPath = decodeURIComponent(req.headers['x-doc-path'] || '')
172
+ const fileName = decodeURIComponent(req.headers['x-file-name'] || `img-${Date.now()}.png`)
173
+
174
+ if (!docPath) {
175
+ res.statusCode = 400; res.end('缺少文档路径'); return
176
+ }
177
+
178
+ const docDir = dirname(resolve(docsRoot, docPath))
179
+ if (!docDir.startsWith(docsRoot)) {
180
+ res.statusCode = 403; res.end('禁止访问'); return
181
+ }
182
+
183
+ const assetsDir = resolve(docDir, 'assets')
184
+ fs.mkdirSync(assetsDir, { recursive: true })
185
+
186
+ const ext = docPath.includes('.') ? `.${fileName.split('.').pop()}` : '.png'
187
+ const baseName = `img-${Date.now()}`
188
+ const targetName = `${baseName}${ext.startsWith('.') ? ext : '.' + ext}`
189
+ const targetPath = resolve(assetsDir, targetName)
190
+
191
+ fs.writeFileSync(targetPath, body)
192
+
193
+ // 返回相对于文档根目录的路径(对路径各段做 URL 编码)
194
+ const docDirRel = dirname(docPath)
195
+ const encodedDir = docDirRel && docDirRel !== '.'
196
+ ? docDirRel.split('/').map(encodeURIComponent).join('/')
197
+ : ''
198
+ const imageUrl = encodedDir
199
+ ? `/@user-docs/${encodedDir}/assets/${targetName}`
200
+ : `/@user-docs/assets/${targetName}`
201
+
202
+ res.setHeader('Content-Type', 'application/json; charset=utf-8')
203
+ res.end(JSON.stringify({ url: imageUrl }))
204
+ } catch (e) {
205
+ res.statusCode = 500; res.end(e.message)
206
+ }
207
+ })
208
+ return
209
+ }
152
210
  // 文档内容
153
211
  if (req.url?.startsWith('/@user-docs/')) {
154
- const filePath = resolve(userDir, decodeURIComponent(req.url.replace('/@user-docs/', '')))
212
+ const filePath = resolve(docsRoot, decodeURIComponent(req.url.replace('/@user-docs/', '')))
155
213
  if (fs.existsSync(filePath)) {
156
- res.setHeader('Content-Type', 'text/plain; charset=utf-8')
157
- res.end(fs.readFileSync(filePath, 'utf-8'))
214
+ // 根据扩展名设置 Content-Type
215
+ const extName = filePath.split('.').pop().toLowerCase()
216
+ const mimeMap = {
217
+ md: 'text/plain; charset=utf-8',
218
+ png: 'image/png',
219
+ jpg: 'image/jpeg',
220
+ jpeg: 'image/jpeg',
221
+ gif: 'image/gif',
222
+ webp: 'image/webp',
223
+ svg: 'image/svg+xml',
224
+ bmp: 'image/bmp',
225
+ }
226
+ const contentType = mimeMap[extName] || 'application/octet-stream'
227
+ res.setHeader('Content-Type', contentType)
228
+ // 附带最后修改时间
229
+ const stat = fs.statSync(filePath)
230
+ res.setHeader('X-Last-Modified', stat.mtime.toISOString())
231
+ // 文本文件用 utf-8 读取,二进制文件直接读取
232
+ if (contentType.startsWith('text/')) {
233
+ res.end(fs.readFileSync(filePath, 'utf-8'))
234
+ } else {
235
+ res.end(fs.readFileSync(filePath))
236
+ }
158
237
  return
159
238
  }
160
239
  }
@@ -178,10 +257,14 @@ function md2uiPlugin(siteConfig) {
178
257
  }
179
258
 
180
259
  async function start() {
260
+ // 解析参数,支持位置参数指定扫描目录
261
+ const cliArgs = parseArgs()
262
+ const scanDir = cliArgs.dir ? resolve(userDir, cliArgs.dir) : userDir
263
+
181
264
  console.log(`\n md2ui - Markdown 文档预览工具\n`)
182
- console.log(` 扫描目录: ${userDir}\n`)
265
+ console.log(` 扫描目录: ${scanDir}\n`)
183
266
 
184
- if (!hasMdFiles(userDir)) {
267
+ if (!hasMdFiles(scanDir)) {
185
268
  console.log(' 当前目录下没有找到 Markdown 文件 (.md)\n')
186
269
  console.log(' 请在包含 .md 文件的目录中运行此命令\n')
187
270
  process.exit(1)
@@ -189,15 +272,21 @@ async function start() {
189
272
 
190
273
  // 加载配置
191
274
  const userConfig = await loadUserConfig()
192
- const cliArgs = parseArgs()
193
275
  const siteConfig = { ...defaultConfig, ...userConfig, ...cliArgs }
276
+ delete siteConfig.dir
194
277
 
195
278
  const server = await createServer({
196
279
  root: pkgRoot,
197
- configFile: resolve(pkgRoot, 'vite.config.js'),
198
- plugins: [md2uiPlugin(siteConfig)],
280
+ configFile: false,
281
+ plugins: [
282
+ (await import('@vitejs/plugin-vue')).default(),
283
+ md2uiPlugin(siteConfig, scanDir)
284
+ ],
199
285
  server: {
200
286
  port: siteConfig.port
287
+ },
288
+ optimizeDeps: {
289
+ include: ['vue', 'marked', 'mermaid']
201
290
  }
202
291
  })
203
292
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "md2ui",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
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
- "mermaid": "^10.6.1",
57
+ "mermaid": "^11.14.0",
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
  }