md2ui 1.0.18 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -58
- package/bin/build.js +95 -9
- package/bin/md2ui.js +102 -13
- package/package.json +24 -10
- package/public/docs/00-/345/277/253/351/200/237/345/274/200/345/247/213.md +48 -28
- package/public/docs/01-/345/212/237/350/203/275/347/211/271/346/200/247.md +55 -40
- package/public/docs/02-Markdown/346/270/262/346/237/223/00-/345/237/272/347/241/200/350/257/255/346/263/225.md +88 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/01-/344/273/243/347/240/201/345/235/227.md +91 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/02-/350/241/250/346/240/274.md +187 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/03-Mermaid/345/233/276/350/241/250.md +101 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/04-Frontmatter.md +32 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/05-/346/225/260/345/255/246/345/205/254/345/274/217.md +47 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/06-Mermaid/345/244/215/346/235/202/345/233/276/350/241/250/346/265/213/350/257/225.md +1376 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/assets/img-1777383093712.png +0 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/00-/344/270/211/346/240/217/345/270/203/345/261/200.md +33 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/01-/347/233/256/345/275/225/346/240/221/345/257/274/350/210/252.md +43 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/02-/346/226/207/346/241/243/345/244/247/347/272/262.md +51 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/03-/344/270/212/344/270/213/347/257/207/345/257/274/350/210/252.md +29 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/04-/347/253/231/345/206/205/351/223/276/346/216/245.md +39 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/05-/345/244/247/347/272/262/345/216/213/345/212/233/346/265/213/350/257/225.md +340 -0
- package/public/docs/04-/346/220/234/347/264/242/345/212/237/350/203/275/00-/345/205/250/346/226/207/346/220/234/347/264/242.md +46 -0
- package/public/docs/05-/347/274/226/350/276/221/345/212/237/350/203/275/00-/347/274/226/350/276/221/345/231/250/345/237/272/347/241/200.md +65 -0
- package/public/docs/05-/347/274/226/350/276/221/345/212/237/350/203/275/01-/350/207/252/345/212/250/344/277/235/345/255/230.md +38 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/00-/351/230/205/350/257/273/350/277/233/345/272/246.md +43 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/01-/345/233/276/347/211/207/346/224/276/345/244/247.md +40 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/02-/350/277/224/345/233/236/351/241/266/351/203/250.md +38 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/assets/img-1777261394722.png +0 -0
- package/public/docs/07-/347/247/273/345/212/250/347/253/257/351/200/202/351/205/215/00-/345/223/215/345/272/224/345/274/217/345/270/203/345/261/200.md +37 -0
- package/public/docs/08-/346/226/207/346/241/243/347/256/241/347/220/206/00-/346/226/260/345/273/272/344/270/216/345/210/240/351/231/244.md +47 -0
- package/public/docs/09-/345/257/274/345/207/272/345/212/237/350/203/275/00-/345/257/274/345/207/272Word.md +77 -0
- package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/00-CLI/345/267/245/345/205/267.md +52 -0
- package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/01-SSG/351/235/231/346/200/201/346/236/204/345/273/272.md +44 -0
- package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/02-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md +58 -0
- package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/00-/344/270/200/347/272/247/346/226/207/346/241/243.md +20 -0
- package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/01-/345/255/220/347/233/256/345/275/225/00-/344/272/214/347/272/247/346/226/207/346/241/243.md +13 -0
- package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/01-/345/255/220/347/233/256/345/275/225/01-/346/267/261/345/261/202/345/265/214/345/245/227/00-/344/270/211/347/272/247/346/226/207/346/241/243.md +23 -0
- package/src/App.vue +111 -12
- package/src/components/AppSidebar.vue +181 -21
- package/src/components/CodeBlockNodeView.vue +72 -0
- package/src/components/DocContent.vue +25 -14
- package/src/components/EditorContent.vue +257 -0
- package/src/components/EditorToolbar.vue +264 -0
- package/src/components/ImageZoom.vue +88 -5
- package/src/components/MathBlockNodeView.vue +160 -0
- package/src/components/MathInlineNodeView.vue +145 -0
- package/src/components/MermaidNodeView.vue +157 -0
- package/src/components/MobileSearch.vue +97 -0
- package/src/components/TableOfContents.vue +174 -32
- package/src/components/TopBar.vue +69 -4
- package/src/components/TreeNode.vue +232 -39
- package/src/components/WelcomePage.vue +2 -2
- package/src/composables/useDocHash.js +9 -1
- package/src/composables/useDocManager.js +452 -105
- package/src/composables/useDocTree.js +33 -2
- package/src/composables/useExportWord.js +73 -10
- package/src/composables/useFileWatcher.js +45 -0
- package/src/composables/useFrontmatter.js +2 -2
- package/src/composables/useMarkdown.js +450 -52
- package/src/composables/useMermaidCache.js +15 -0
- package/src/composables/useScroll.js +354 -27
- package/src/composables/useSearch.js +12 -11
- package/src/config.js +1 -4
- package/src/extensions/CodeBlockCustom.js +113 -0
- package/src/extensions/MathBlock.js +107 -0
- package/src/extensions/MathInline.js +100 -0
- package/src/extensions/MermaidBlock.js +73 -0
- package/src/extensions/TableControls.js +670 -0
- package/src/services/DocService.js +168 -0
- package/src/style.css +2416 -36
- package/src/utils/imageConverter.js +129 -0
- package/vite-plugin-doc-api.js +369 -0
- package/vite.config.js +7 -2
- package/public/docs/02-Mermaid/345/233/276/350/241/250.md +0 -102
- package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/01-/347/233/256/345/275/225/347/273/223/346/236/204.md +0 -55
- package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/02-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md +0 -63
- package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/03-/351/203/250/347/275/262/346/226/271/346/241/210.md +0 -73
- package/public/docs/04-API/345/217/202/350/200/203/01-/347/273/204/344/273/266API.md +0 -80
- package/public/docs/04-API/345/217/202/350/200/203/02-Composables.md +0 -92
- package/src/api/docs.js +0 -106
- package/src/components/SearchPanel.vue +0 -90
package/README.md
CHANGED
|
@@ -1,81 +1,75 @@
|
|
|
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` 文件转换为可预览、可编辑、可搜索的文档站点。
|
|
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
|
-

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

|
|
11
|
+
AI 编程时代,Cursor、Copilot、Kiro 等工具正在改变我们的开发方式。与 AI 协作的过程中,会产生大量的 Markdown 文档 —— 需求文档、设计方案、API 文档、会议纪要、技术调研……
|
|
30
12
|
|
|
31
|
-
|
|
13
|
+
这些 `.md` 文件散落在项目各处,带来了一系列痛点:
|
|
32
14
|
|
|
33
|
-
|
|
15
|
+
- 文件越来越多,找一篇文档要翻半天
|
|
16
|
+
- IDE 的 Markdown 预览体验一般,表格、流程图、数学公式渲染不理想
|
|
17
|
+
- 想快速改几个字,还得切回编辑器找到对应文件
|
|
18
|
+
- 文档之间缺乏导航关系,无法形成知识体系
|
|
19
|
+
- 想分享给团队成员,还得额外搭建文档站
|
|
34
20
|
|
|
35
|
-
|
|
21
|
+
md2ui 就是为了解决这些问题而生的。把它指向你的文档目录,立刻获得一个功能完整的文档站点。
|
|
36
22
|
|
|
37
|
-
|
|
23
|
+
## 核心能力
|
|
38
24
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+

|
|
40
|
+
编辑模式
|
|
50
41
|
|
|
51
|
-

|
|
52
43
|
|
|
53
|
-
|
|
44
|
+
## 典型使用场景
|
|
54
45
|
|
|
55
|
-
|
|
46
|
+
**AI 辅助开发的文档管理**
|
|
56
47
|
|
|
57
|
-
|
|
48
|
+
用 AI 生成的需求文档、设计方案、技术调研统一放到一个目录,md2ui 提供即时预览和编辑,让文档真正流动起来。
|
|
58
49
|
|
|
59
|
-
|
|
50
|
+
**个人知识库**
|
|
60
51
|
|
|
61
|
-
|
|
52
|
+
日常笔记、学习记录、读书摘要,用 Markdown 写完丢进文件夹,md2ui 自动组织成可浏览的知识站点。
|
|
62
53
|
|
|
63
|
-
|
|
54
|
+
**项目文档站**
|
|
64
55
|
|
|
65
|
-
|
|
56
|
+
API 文档、部署指南、开发规范,`md2ui build` 一键构建为静态站点,部署到任意服务器。
|
|
66
57
|
|
|
67
|
-
|
|
58
|
+
**团队协作文档**
|
|
68
59
|
|
|
69
|
-
|
|
60
|
+
配合 Git 管理文档版本,本地用 md2ui 预览和编辑,提交后自动构建部署。
|
|
70
61
|
|
|
71
|
-
##
|
|
62
|
+
## 快速开始
|
|
72
63
|
|
|
73
|
-
###
|
|
64
|
+
### 安装
|
|
74
65
|
|
|
75
66
|
```bash
|
|
76
|
-
|
|
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
|
-
-
|
|
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/
|
|
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="${
|
|
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(` 扫描目录: ${
|
|
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(
|
|
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(
|
|
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.
|
|
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(
|
|
212
|
+
const filePath = resolve(docsRoot, decodeURIComponent(req.url.replace('/@user-docs/', '')))
|
|
155
213
|
if (fs.existsSync(filePath)) {
|
|
156
|
-
|
|
157
|
-
|
|
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(` 扫描目录: ${
|
|
265
|
+
console.log(` 扫描目录: ${scanDir}\n`)
|
|
183
266
|
|
|
184
|
-
if (!hasMdFiles(
|
|
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:
|
|
198
|
-
plugins: [
|
|
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.
|
|
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/
|
|
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
|
-
"mermaid": "^
|
|
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
|
}
|