md2ui 1.0.7 → 1.0.9
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 +104 -38
- package/bin/build.js +608 -0
- package/bin/md2ui.js +112 -63
- package/index.html +43 -1
- package/package.json +6 -5
- package/src/App.vue +106 -250
- package/src/api/docs.js +2 -1
- package/src/components/AppSidebar.vue +102 -0
- package/src/components/DocContent.vue +39 -0
- package/src/components/Logo.vue +5 -9
- package/src/components/MobileHeader.vue +20 -0
- package/src/components/MobileToc.vue +40 -0
- package/src/components/SearchPanel.vue +90 -0
- package/src/components/TableOfContents.vue +2 -2
- package/src/components/TopBar.vue +144 -0
- package/src/components/WelcomePage.vue +251 -0
- package/src/composables/useDocHash.js +66 -0
- package/src/composables/useDocManager.js +280 -0
- package/src/composables/useDocTree.js +96 -0
- package/src/composables/useFrontmatter.js +41 -0
- package/src/composables/useMarkdown.js +316 -94
- package/src/composables/useMobile.js +29 -0
- package/src/composables/useScroll.js +9 -15
- package/src/composables/useSearch.js +133 -0
- package/src/hljs-theme.css +104 -0
- package/src/main.js +1 -0
- package/src/style.css +935 -213
package/bin/md2ui.js
CHANGED
|
@@ -1,61 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// 子命令路由:build 命令走独立的 SSG 构建流程
|
|
4
|
+
const subCommand = process.argv[2]
|
|
5
|
+
if (subCommand === 'build') {
|
|
6
|
+
await import('./build.js')
|
|
7
|
+
process.exit(0)
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
import { createServer } from 'vite'
|
|
4
11
|
import { fileURLToPath } from 'url'
|
|
5
12
|
import { dirname, resolve } from 'path'
|
|
6
13
|
import fs from 'fs'
|
|
14
|
+
import { exec } from 'child_process'
|
|
15
|
+
import { pathToFileURL } from 'url'
|
|
7
16
|
|
|
8
|
-
// 获取 CLI
|
|
17
|
+
// 获取 CLI 工具所在目录
|
|
9
18
|
const __filename = fileURLToPath(import.meta.url)
|
|
10
19
|
const __dirname = dirname(__filename)
|
|
11
20
|
const pkgRoot = resolve(__dirname, '..')
|
|
12
21
|
|
|
13
|
-
// 导入共享配置
|
|
14
|
-
const { config } = await import(resolve(pkgRoot, 'src/config.js'))
|
|
15
|
-
|
|
16
22
|
// 用户执行命令的目录
|
|
17
23
|
const userDir = process.cwd()
|
|
18
24
|
|
|
19
|
-
//
|
|
20
|
-
const
|
|
21
|
-
|
|
25
|
+
// 默认配置
|
|
26
|
+
const defaultConfig = {
|
|
27
|
+
title: 'md2ui',
|
|
28
|
+
port: 3000,
|
|
29
|
+
folderExpanded: false,
|
|
30
|
+
github: '',
|
|
31
|
+
footer: '',
|
|
32
|
+
themeColor: '#3eaf7c'
|
|
33
|
+
}
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
// 加载用户配置文件(md2ui.config.js 或 .md2uirc.json)
|
|
36
|
+
async function loadUserConfig() {
|
|
37
|
+
// 尝试 md2ui.config.js
|
|
38
|
+
const jsPath = resolve(userDir, 'md2ui.config.js')
|
|
39
|
+
if (fs.existsSync(jsPath)) {
|
|
40
|
+
try {
|
|
41
|
+
const mod = await import(pathToFileURL(jsPath).href)
|
|
42
|
+
console.log(' 配置文件: md2ui.config.js\n')
|
|
43
|
+
return mod.default || mod
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.warn(' 配置文件加载失败:', e.message, '\n')
|
|
46
|
+
}
|
|
26
47
|
}
|
|
48
|
+
// 尝试 .md2uirc.json
|
|
49
|
+
const jsonPath = resolve(userDir, '.md2uirc.json')
|
|
50
|
+
if (fs.existsSync(jsonPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = fs.readFileSync(jsonPath, 'utf-8')
|
|
53
|
+
console.log(' 配置文件: .md2uirc.json\n')
|
|
54
|
+
return JSON.parse(raw)
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.warn(' 配置文件加载失败:', e.message, '\n')
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {}
|
|
27
60
|
}
|
|
28
61
|
|
|
29
|
-
//
|
|
30
|
-
function
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (id === virtualModuleId) {
|
|
38
|
-
return resolvedVirtualModuleId
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
load(id) {
|
|
42
|
-
if (id === resolvedVirtualModuleId) {
|
|
43
|
-
// 扫描用户目录下的 md 文件
|
|
44
|
-
const docs = scanDocs(userDir)
|
|
45
|
-
return `export default ${JSON.stringify(docs)}`
|
|
46
|
-
}
|
|
62
|
+
// 解析命令行参数
|
|
63
|
+
function parseArgs() {
|
|
64
|
+
const args = process.argv.slice(2)
|
|
65
|
+
const result = {}
|
|
66
|
+
for (let i = 0; i < args.length; i++) {
|
|
67
|
+
if (args[i] === '-p' || args[i] === '--port') {
|
|
68
|
+
result.port = parseInt(args[i + 1]) || undefined
|
|
69
|
+
i++
|
|
47
70
|
}
|
|
48
71
|
}
|
|
72
|
+
return result
|
|
49
73
|
}
|
|
50
74
|
|
|
51
75
|
// 扫描目录下的 md 文件
|
|
52
|
-
function scanDocs(dir, basePath = '', level = 0) {
|
|
76
|
+
function scanDocs(dir, basePath = '', level = 0, folderExpanded = false) {
|
|
53
77
|
const items = []
|
|
54
|
-
|
|
55
78
|
if (!fs.existsSync(dir)) return items
|
|
56
|
-
|
|
79
|
+
|
|
57
80
|
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
58
|
-
.filter(e =>
|
|
81
|
+
.filter(e => e.name !== 'node_modules')
|
|
59
82
|
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
|
|
60
83
|
|
|
61
84
|
for (const entry of entries) {
|
|
@@ -63,8 +86,7 @@ function scanDocs(dir, basePath = '', level = 0) {
|
|
|
63
86
|
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name
|
|
64
87
|
|
|
65
88
|
if (entry.isDirectory()) {
|
|
66
|
-
const children = scanDocs(fullPath, relativePath, level + 1)
|
|
67
|
-
// 只有包含 md 文件的文件夹才加入目录
|
|
89
|
+
const children = scanDocs(fullPath, relativePath, level + 1, folderExpanded)
|
|
68
90
|
if (children.length > 0) {
|
|
69
91
|
const match = entry.name.match(/^(\d+)-(.+)$/)
|
|
70
92
|
items.push({
|
|
@@ -73,7 +95,7 @@ function scanDocs(dir, basePath = '', level = 0) {
|
|
|
73
95
|
order: match ? parseInt(match[1]) : 999,
|
|
74
96
|
type: 'folder',
|
|
75
97
|
level,
|
|
76
|
-
expanded:
|
|
98
|
+
expanded: folderExpanded,
|
|
77
99
|
children
|
|
78
100
|
})
|
|
79
101
|
}
|
|
@@ -91,25 +113,43 @@ function scanDocs(dir, basePath = '', level = 0) {
|
|
|
91
113
|
}
|
|
92
114
|
}
|
|
93
115
|
|
|
94
|
-
// 按 order 排序
|
|
95
116
|
items.sort((a, b) => a.order - b.order)
|
|
96
117
|
return items
|
|
97
118
|
}
|
|
98
119
|
|
|
99
|
-
//
|
|
100
|
-
function
|
|
120
|
+
// 检查目录下是否有 md 文件
|
|
121
|
+
function hasMdFiles(dir) {
|
|
122
|
+
if (!fs.existsSync(dir)) return false
|
|
123
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
124
|
+
.filter(e => e.name !== 'node_modules')
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (entry.isFile() && entry.name.endsWith('.md')) return true
|
|
127
|
+
if (entry.isDirectory() && hasMdFiles(resolve(dir, entry.name))) return true
|
|
128
|
+
}
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Vite 插件:提供用户文档 API + 配置 API + 热更新
|
|
133
|
+
function md2uiPlugin(siteConfig) {
|
|
101
134
|
return {
|
|
102
|
-
name: '
|
|
135
|
+
name: 'md2ui-server',
|
|
103
136
|
configureServer(server) {
|
|
137
|
+
// API 中间件
|
|
104
138
|
server.middlewares.use((req, res, next) => {
|
|
105
|
-
//
|
|
139
|
+
// 文档列表 API
|
|
106
140
|
if (req.url === '/@user-docs-list') {
|
|
107
|
-
const docs = scanDocs(userDir)
|
|
141
|
+
const docs = scanDocs(userDir, '', 0, siteConfig.folderExpanded)
|
|
108
142
|
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
109
143
|
res.end(JSON.stringify(docs))
|
|
110
144
|
return
|
|
111
145
|
}
|
|
112
|
-
//
|
|
146
|
+
// 站点配置 API
|
|
147
|
+
if (req.url === '/@site-config') {
|
|
148
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
149
|
+
res.end(JSON.stringify(siteConfig))
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
// 文档内容
|
|
113
153
|
if (req.url?.startsWith('/@user-docs/')) {
|
|
114
154
|
const filePath = resolve(userDir, decodeURIComponent(req.url.replace('/@user-docs/', '')))
|
|
115
155
|
if (fs.existsSync(filePath)) {
|
|
@@ -120,52 +160,61 @@ function serveUserDocs() {
|
|
|
120
160
|
}
|
|
121
161
|
next()
|
|
122
162
|
})
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
163
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (entry.isDirectory()) {
|
|
139
|
-
if (hasMdFiles(resolve(dir, entry.name))) {
|
|
140
|
-
return true
|
|
164
|
+
// SPA fallback
|
|
165
|
+
return () => {
|
|
166
|
+
server.middlewares.use((req, res, next) => {
|
|
167
|
+
const url = req.url?.split('?')[0] || ''
|
|
168
|
+
if (url.startsWith('/@') || url.startsWith('/src/') || url.startsWith('/node_modules/') || url.match(/\.\w+$/)) {
|
|
169
|
+
next()
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
req.url = '/index.html'
|
|
173
|
+
next()
|
|
174
|
+
})
|
|
141
175
|
}
|
|
142
176
|
}
|
|
143
177
|
}
|
|
144
|
-
return false
|
|
145
178
|
}
|
|
146
179
|
|
|
147
180
|
async function start() {
|
|
148
181
|
console.log(`\n md2ui - Markdown 文档预览工具\n`)
|
|
149
182
|
console.log(` 扫描目录: ${userDir}\n`)
|
|
150
183
|
|
|
151
|
-
// 检查是否有 md 文件
|
|
152
184
|
if (!hasMdFiles(userDir)) {
|
|
153
185
|
console.log(' 当前目录下没有找到 Markdown 文件 (.md)\n')
|
|
154
186
|
console.log(' 请在包含 .md 文件的目录中运行此命令\n')
|
|
155
187
|
process.exit(1)
|
|
156
188
|
}
|
|
157
189
|
|
|
190
|
+
// 加载配置
|
|
191
|
+
const userConfig = await loadUserConfig()
|
|
192
|
+
const cliArgs = parseArgs()
|
|
193
|
+
const siteConfig = { ...defaultConfig, ...userConfig, ...cliArgs }
|
|
194
|
+
|
|
158
195
|
const server = await createServer({
|
|
159
196
|
root: pkgRoot,
|
|
160
197
|
configFile: resolve(pkgRoot, 'vite.config.js'),
|
|
161
|
-
plugins: [
|
|
198
|
+
plugins: [md2uiPlugin(siteConfig)],
|
|
162
199
|
server: {
|
|
163
|
-
port
|
|
200
|
+
port: siteConfig.port
|
|
164
201
|
}
|
|
165
202
|
})
|
|
166
203
|
|
|
167
204
|
await server.listen()
|
|
168
205
|
server.printUrls()
|
|
206
|
+
|
|
207
|
+
if (siteConfig.title !== defaultConfig.title) {
|
|
208
|
+
console.log(` 站点标题: ${siteConfig.title}`)
|
|
209
|
+
}
|
|
210
|
+
console.log('')
|
|
211
|
+
|
|
212
|
+
// 自动打开浏览器
|
|
213
|
+
const address = server.httpServer.address()
|
|
214
|
+
const url = `http://localhost:${address.port}`
|
|
215
|
+
const platform = process.platform
|
|
216
|
+
const cmd = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open'
|
|
217
|
+
exec(`${cmd} ${url}`)
|
|
169
218
|
}
|
|
170
219
|
|
|
171
220
|
start().catch(console.error)
|
package/index.html
CHANGED
|
@@ -6,9 +6,51 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/logo.svg">
|
|
7
7
|
<title>md2ui - Markdown 文档渲染系统</title>
|
|
8
8
|
<meta name="description" content="将本地 Markdown 文档转换为美观的 HTML 页面">
|
|
9
|
+
<!-- 内联骨架屏样式,防止白屏闪烁 -->
|
|
10
|
+
<style>
|
|
11
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
12
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; background: #ffffff; }
|
|
13
|
+
.app-skeleton {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
height: 100vh;
|
|
17
|
+
}
|
|
18
|
+
.skeleton-topbar {
|
|
19
|
+
height: 49px;
|
|
20
|
+
background: #f6f8fa;
|
|
21
|
+
border-bottom: 1px solid #d0d7de;
|
|
22
|
+
flex-shrink: 0;
|
|
23
|
+
}
|
|
24
|
+
.skeleton-body {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex: 1;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
}
|
|
29
|
+
.skeleton-sidebar {
|
|
30
|
+
width: 260px;
|
|
31
|
+
background: #f6f8fa;
|
|
32
|
+
border-right: 1px solid #d0d7de;
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
}
|
|
35
|
+
.skeleton-content {
|
|
36
|
+
flex: 1;
|
|
37
|
+
}
|
|
38
|
+
@media (max-width: 768px) {
|
|
39
|
+
.skeleton-sidebar { display: none; }
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
9
42
|
</head>
|
|
10
43
|
<body>
|
|
11
|
-
<div id="app"
|
|
44
|
+
<div id="app">
|
|
45
|
+
<!-- 骨架屏:Vue 挂载后自动替换 -->
|
|
46
|
+
<div class="app-skeleton">
|
|
47
|
+
<div class="skeleton-topbar"></div>
|
|
48
|
+
<div class="skeleton-body">
|
|
49
|
+
<div class="skeleton-sidebar"></div>
|
|
50
|
+
<div class="skeleton-content"></div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
12
54
|
<script type="module" src="/src/main.js"></script>
|
|
13
55
|
</body>
|
|
14
56
|
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md2ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "将本地 Markdown 文档转换为美观的 HTML 页面",
|
|
6
6
|
"author": "",
|
|
@@ -33,14 +33,15 @@
|
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@vitejs/plugin-vue": "^5.0.0",
|
|
36
|
-
"
|
|
36
|
+
"flexsearch": "^0.8.212",
|
|
37
|
+
"github-slugger": "^2.0.0",
|
|
38
|
+
"highlight.js": "^11.11.1",
|
|
39
|
+
"jsdom": "^28.1.0",
|
|
37
40
|
"lucide-vue-next": "^0.556.0",
|
|
38
41
|
"marked": "^11.1.1",
|
|
39
42
|
"mermaid": "^10.6.1",
|
|
43
|
+
"minisearch": "^7.2.0",
|
|
40
44
|
"vite": "^5.0.0",
|
|
41
45
|
"vue": "^3.4.0"
|
|
42
|
-
},
|
|
43
|
-
"devDependencies": {
|
|
44
|
-
"@types/crypto-js": "^4.2.2"
|
|
45
46
|
}
|
|
46
47
|
}
|