zhangdocs 1.1.31 → 1.1.34

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/.bin/zhangdocs.js CHANGED
@@ -18,9 +18,11 @@ commander
18
18
  .option('-b, build', 'Markdown produces static pages document.')
19
19
  .option('-w, watch', 'Listener "md" file is automatically generated pages.')
20
20
  .option('-s, server', 'Open local static html server.')
21
+ .option('-d, --daemon', 'Run server in background (use with -s). Close terminal safely.')
22
+ .option('--stop', 'Stop background server started with -s -d.')
21
23
  .option('-c, clean', 'Clear the generate static files.')
22
24
  .option('-t, theme', 'Choose a theme.')
23
- .option('-d, deploy', 'Publish to a gh-pages branch on GitHub.')
25
+ .option('--deploy', 'Publish to a gh-pages branch on GitHub.')
24
26
  .option('-p, pdf', 'PDF generation.')
25
27
 
26
28
  commander
@@ -31,9 +33,11 @@ commander
31
33
  log(' $ zhangdocs init [path] ');
32
34
  log(' $ zhangdocs init [path] -C ~/zhangdocs/');
33
35
  log(' $ zhangdocs watch');
34
- log(' $ zhangdocs server');
35
- log(' $ zhangdocs clean');
36
- log(' $ zhangdocs deploy');
36
+ log(' $ doc -s');
37
+ log(' $ doc -s -d # server in background');
38
+ log(' $ doc -s --stop # stop background server');
39
+ log(' $ doc clean');
40
+ log(' $ doc --deploy');
37
41
  log(' $ zhangdocs theme');
38
42
  log(' $ zhangdocs -t ~/git/zhangdocs-theme-slate/');
39
43
 
@@ -0,0 +1 @@
1
+ [2026-05-16T11:05:28.652Z] doc server started (PID 34124)
package/README.md CHANGED
@@ -85,11 +85,25 @@ doc build
85
85
  ### 4. 启动开发服务器
86
86
 
87
87
  ```bash
88
- doc server
88
+ doc -s
89
89
  ```
90
90
 
91
91
  这将启动一个本地服务器,并监听文件变化自动重新构建。
92
92
 
93
+ 后台运行(关闭命令行窗口后服务仍继续):
94
+
95
+ ```bash
96
+ doc -s -d
97
+ ```
98
+
99
+ 停止后台服务:
100
+
101
+ ```bash
102
+ doc -s --stop
103
+ ```
104
+
105
+ 日志文件位于项目目录下的 `.zhangdocs/server.log`。
106
+
93
107
  ### 5. 监听文件变化
94
108
 
95
109
  ```bash
@@ -105,9 +119,11 @@ doc watch
105
119
  | `doc init` | 初始化项目 |
106
120
  | `doc build` | 构建静态页面 |
107
121
  | `doc watch` | 监听文件变化并自动构建 |
108
- | `doc server` | 启动开发服务器(包含 watch 功能) |
122
+ | `doc -s` | 启动开发服务器(包含 watch 功能) |
123
+ | `doc -s -d` | 后台启动开发服务器,可关闭终端 |
124
+ | `doc -s --stop` | 停止后台开发服务器 |
109
125
  | `doc clean` | 清理构建文件 |
110
- | `doc deploy` | 部署文档 |
126
+ | `doc --deploy` | 部署文档 |
111
127
  | `doc theme` | 主题管理 |
112
128
  | `doc -V` | 查看版本号 |
113
129
 
package/index.js CHANGED
@@ -8,7 +8,7 @@ var pdf = require('./lib/pdf');
8
8
  var theme = require('./lib/theme');
9
9
  var color = require('colors-cli');
10
10
  var path = require('path');
11
- var server = require('ssr');
11
+ var runServer = require('./lib/server');
12
12
 
13
13
  module.exports = function (commander) {
14
14
  var pkgurl = path.resolve('package.json');
@@ -36,9 +36,15 @@ module.exports = function (commander) {
36
36
  if (commander.watch) {
37
37
  return watch(commander, build);
38
38
  }
39
+ if (commander.stop) {
40
+ return runServer.stop();
41
+ }
39
42
  if (commander.server) {
40
- server();
41
- return watch(commander, build);
43
+ return runServer(commander, {
44
+ build: build,
45
+ server: require('ssr'),
46
+ watch: watch
47
+ });
42
48
  }
43
49
  if (commander.clean) {
44
50
  return clean(commander);
package/lib/build.js CHANGED
@@ -144,15 +144,12 @@ function build(commander, changeFile) {
144
144
  var _path;
145
145
  var tocHTML = '';//单页面导航静态页面
146
146
  var navHTML = '';
147
- var pathArr = file.readMDSync(todir + 'md/');
148
-
149
-
150
- if (changeFile) pathArr = [changeFile];
147
+ var allPathArr = file.readMDSync(todir + 'md/');
151
148
 
152
149
  // 按文件名前面的数字排序
153
150
  // 如果文件名以数字开头(格式:数字.标题.md),按数字排序
154
151
  // 如果文件名不以数字开头,排在有序号的后面
155
- pathArr.sort(function(a, b) {
152
+ allPathArr.sort(function(a, b) {
156
153
  var aBasename = path.basename(a, '.md');
157
154
  var bBasename = path.basename(b, '.md');
158
155
 
@@ -179,8 +176,22 @@ function build(commander, changeFile) {
179
176
  return aBasename.localeCompare(bBasename);
180
177
  });
181
178
 
179
+ // 增量编译时只写变更文件,但上一节/下一节、导航使用全量文件列表计算
180
+ var buildPathArr = allPathArr;
181
+ if (changeFile) {
182
+ var normalizedChangeFile = path.normalize(changeFile);
183
+ var matchedFile = null;
184
+ for (var c = 0; c < allPathArr.length; c++) {
185
+ if (path.normalize(allPathArr[c]) === normalizedChangeFile) {
186
+ matchedFile = allPathArr[c];
187
+ break;
188
+ }
189
+ }
190
+ buildPathArr = [matchedFile || normalizedChangeFile];
191
+ }
192
+
182
193
  // 先处理所有md文件,包括第一个md文件
183
- pathArr.forEach(function (item, idx) {
194
+ buildPathArr.forEach(function (item) {
184
195
  //菜单层级
185
196
  var isIndex = false
186
197
  var len = item.replace(process.cwd() + '/md/', '').split('/').length;
@@ -195,7 +206,7 @@ function build(commander, changeFile) {
195
206
  var _path = path.normalize(process.cwd() + '/html/' + itemRel).replace('.md', ".html");
196
207
 
197
208
  //导航菜单生成 - 传入HTML路径作为当前页面
198
- var navHTML = nav(zhangdocsmd, _path, pathArr, len);
209
+ var navHTML = nav(zhangdocsmd, _path, allPathArr, len);
199
210
 
200
211
  let mdContent = file.read(item);
201
212
  // 自动把 \[ ... \] 替换为 $$ ... $$
@@ -225,9 +236,17 @@ function build(commander, changeFile) {
225
236
  var prevTitle = null;
226
237
  var nextTitle = null;
227
238
 
228
- if (idx > 0) {
239
+ var currentIndex = -1;
240
+ for (var i = 0; i < allPathArr.length; i++) {
241
+ if (path.normalize(allPathArr[i]) === path.normalize(item)) {
242
+ currentIndex = i;
243
+ break;
244
+ }
245
+ }
246
+
247
+ if (currentIndex > 0) {
229
248
  // 上一节
230
- var prevItem = pathArr[idx - 1];
249
+ var prevItem = allPathArr[currentIndex - 1];
231
250
  var prevItemRel = path.normalize(prevItem);
232
251
  prevItemRel = prevItemRel.replace(path.normalize(process.cwd() + '/md/'), '');
233
252
  var prevPath = path.normalize(process.cwd() + '/html/' + prevItemRel).replace('.md', ".html");
@@ -247,9 +266,9 @@ function build(commander, changeFile) {
247
266
  prevTitle = path.basename(prevItem, '.md');
248
267
  }
249
268
 
250
- if (idx < pathArr.length - 1) {
269
+ if (currentIndex > -1 && currentIndex < allPathArr.length - 1) {
251
270
  // 下一节
252
- var nextItem = pathArr[idx + 1];
271
+ var nextItem = allPathArr[currentIndex + 1];
253
272
  var nextItemRel = path.normalize(nextItem);
254
273
  nextItemRel = nextItemRel.replace(path.normalize(process.cwd() + '/md/'), '');
255
274
  var nextPath = path.normalize(process.cwd() + '/html/' + nextItemRel).replace('.md', ".html");
@@ -291,8 +310,8 @@ function build(commander, changeFile) {
291
310
 
292
311
  // 单独生成index.html作为目录页面
293
312
  var indexPath = path.normalize(process.cwd() + '/index.html');
294
- var indexNavHTML = nav(zhangdocsmd, indexPath, pathArr, 0);
295
- var indexMarkedstr = generateIndexPage(zhangdocsmd, pathArr, indexPath, file.relativePath(indexPath, process.cwd()));
313
+ var indexNavHTML = nav(zhangdocsmd, indexPath, allPathArr, 0);
314
+ var indexMarkedstr = generateIndexPage(zhangdocsmd, allPathArr, indexPath, file.relativePath(indexPath, process.cwd()));
296
315
 
297
316
  html = file.ejs(template + '/layout.ejs', {
298
317
  title: pkg.name,//项目工程名字
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 后台服务模式子进程:启动静态服务 + 监听 md 变更
3
+ * 由 lib/server.js 以 detached 方式拉起,勿直接调用
4
+ */
5
+ var fs = require('fs');
6
+ var path = require('path');
7
+ var file = require('./file');
8
+ var build = require('./build');
9
+ var watch = require('./watch');
10
+ var server = require('ssr');
11
+
12
+ var pidDir = path.join(process.cwd(), '.zhangdocs');
13
+ var pidFile = path.join(pidDir, 'server.pid');
14
+ var logFile = path.join(pidDir, 'server.log');
15
+
16
+ function appendLog(line) {
17
+ try {
18
+ if (!fs.existsSync(pidDir)) {
19
+ file.mkdirsSync(pidDir, 0o777);
20
+ }
21
+ fs.appendFileSync(logFile, line + '\n', 'utf8');
22
+ } catch (e) {
23
+ // ignore
24
+ }
25
+ }
26
+
27
+ function cleanup() {
28
+ try {
29
+ if (fs.existsSync(pidFile)) {
30
+ fs.unlinkSync(pidFile);
31
+ }
32
+ } catch (e) {
33
+ // ignore
34
+ }
35
+ }
36
+
37
+ process.on('SIGINT', function () {
38
+ cleanup();
39
+ process.exit(0);
40
+ });
41
+ process.on('SIGTERM', function () {
42
+ cleanup();
43
+ process.exit(0);
44
+ });
45
+ process.on('exit', cleanup);
46
+
47
+ try {
48
+ if (!fs.existsSync(pidDir)) {
49
+ file.mkdirsSync(pidDir, 0o777);
50
+ }
51
+ fs.writeFileSync(pidFile, String(process.pid), 'utf8');
52
+
53
+ var commanderStub = {};
54
+ build(commanderStub);
55
+ server();
56
+ watch(commanderStub, build);
57
+
58
+ appendLog('[' + new Date().toISOString() + '] doc server started (PID ' + process.pid + ')');
59
+ } catch (err) {
60
+ appendLog('[' + new Date().toISOString() + '] doc server failed: ' + (err && err.stack ? err.stack : err));
61
+ cleanup();
62
+ process.exit(1);
63
+ }
package/lib/server.js ADDED
@@ -0,0 +1,135 @@
1
+ var fs = require('fs');
2
+ var path = require('path');
3
+ var spawn = require('child_process').spawn;
4
+ var color = require('colors-cli');
5
+ var file = require('./file');
6
+
7
+ var PID_DIR = '.zhangdocs';
8
+ var PID_NAME = 'server.pid';
9
+ var LOG_NAME = 'server.log';
10
+
11
+ function pidPath() {
12
+ return path.join(process.cwd(), PID_DIR, PID_NAME);
13
+ }
14
+
15
+ function logPath() {
16
+ return path.join(process.cwd(), PID_DIR, LOG_NAME);
17
+ }
18
+
19
+ function ensurePidDir() {
20
+ var dir = path.join(process.cwd(), PID_DIR);
21
+ if (!fs.existsSync(dir)) {
22
+ file.mkdirsSync(dir, 0o777);
23
+ }
24
+ return dir;
25
+ }
26
+
27
+ function isProcessAlive(pid) {
28
+ if (!pid || pid <= 0) {
29
+ return false;
30
+ }
31
+ try {
32
+ process.kill(pid, 0);
33
+ return true;
34
+ } catch (e) {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ function readPid() {
40
+ var file = pidPath();
41
+ if (!fs.existsSync(file)) {
42
+ return null;
43
+ }
44
+ var pid = parseInt(String(fs.readFileSync(file, 'utf8')).trim(), 10);
45
+ return isNaN(pid) ? null : pid;
46
+ }
47
+
48
+ function clearPidFile() {
49
+ try {
50
+ if (fs.existsSync(pidPath())) {
51
+ fs.unlinkSync(pidPath());
52
+ }
53
+ } catch (e) {
54
+ // ignore
55
+ }
56
+ }
57
+
58
+ function killPid(pid) {
59
+ if (process.platform === 'win32') {
60
+ spawn('taskkill', ['/PID', String(pid), '/T', '/F'], {
61
+ stdio: 'ignore',
62
+ windowsHide: true
63
+ });
64
+ } else {
65
+ process.kill(pid, 'SIGTERM');
66
+ }
67
+ }
68
+
69
+ function stopBackground() {
70
+ var pid = readPid();
71
+ if (!pid || !isProcessAlive(pid)) {
72
+ clearPidFile();
73
+ console.log(color.yellow('\n 未发现运行中的后台 doc server。\n'));
74
+ return;
75
+ }
76
+ killPid(pid);
77
+ clearPidFile();
78
+ console.log(color.green('\n 已停止后台 doc server (PID ' + pid + ')。\n'));
79
+ }
80
+
81
+ function startForeground(commander, build, server, watch) {
82
+ build(commander);
83
+ server();
84
+ watch(commander, build);
85
+ }
86
+
87
+ function startDaemon(commander, build) {
88
+ var pid = readPid();
89
+ if (pid && isProcessAlive(pid)) {
90
+ console.log(color.yellow('\n doc server 已在后台运行 (PID ' + pid + ')。'));
91
+ console.log(' 日志: ' + logPath());
92
+ console.log(' 停止: doc -s --stop\n');
93
+ return;
94
+ }
95
+ if (pid) {
96
+ clearPidFile();
97
+ }
98
+
99
+ ensurePidDir();
100
+ build(commander);
101
+
102
+ var worker = path.join(__dirname, 'server-worker.js');
103
+ var child = spawn(process.execPath, [worker], {
104
+ cwd: process.cwd(),
105
+ detached: true,
106
+ stdio: 'ignore',
107
+ windowsHide: true,
108
+ env: process.env
109
+ });
110
+
111
+ child.unref();
112
+
113
+ console.log(color.green('\n doc server 已在后台启动。'));
114
+ console.log(' 可关闭当前命令行窗口,服务会继续运行。');
115
+ console.log(' 日志: ' + logPath());
116
+ console.log(' 停止: doc -s --stop\n');
117
+ }
118
+
119
+ /**
120
+ * @param {object} commander
121
+ * @param {{ build: Function, server: Function, watch: Function }} deps
122
+ */
123
+ function runServer(commander, deps) {
124
+ if (commander.stop) {
125
+ return stopBackground();
126
+ }
127
+ if (commander.daemon) {
128
+ return startDaemon(commander, deps.build);
129
+ }
130
+ return startForeground(commander, deps.build, deps.server, deps.watch);
131
+ }
132
+
133
+ runServer.stop = stopBackground;
134
+
135
+ module.exports = runServer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zhangdocs",
3
- "version": "1.1.31",
3
+ "version": "1.1.34",
4
4
  "description": "Simple document generation tool. Dependence Node.js run.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,3 +20,4 @@ Thumbs.db
20
20
  node_modules
21
21
  .cache
22
22
  zhangdocs-theme-*/
23
+ .zhangdocs/
@@ -34,13 +34,23 @@
34
34
  mermaid.initialize({
35
35
  startOnLoad: true,
36
36
  theme: 'default',
37
+ flowchart: {
38
+ useMaxWidth: true,
39
+ htmlLabels: false,
40
+ wrappingWidth: 180,
41
+ nodeSpacing: 45,
42
+ rankSpacing: 50,
43
+ padding: 16
44
+ },
45
+ themeCSS: '.mermaid .label text, .mermaid .nodeLabel { dominant-baseline: central; }',
37
46
  themeVariables: {
38
47
  primaryColor: '#ff6b6b',
39
48
  primaryTextColor: '#333',
40
49
  primaryBorderColor: '#ff6b6b',
41
50
  lineColor: '#333',
42
51
  secondaryColor: '#f8f9fa',
43
- tertiaryColor: '#fff'
52
+ tertiaryColor: '#fff',
53
+ fontSize: '10px'
44
54
  }
45
55
  });
46
56
  });
@@ -103,6 +103,104 @@ style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background:
103
103
 
104
104
  // 标记为已初始化
105
105
  isInitialized = true;
106
+
107
+ function getDirectLink(li) {
108
+ if (!li || !li.children) {
109
+ return null;
110
+ }
111
+ for (var i = 0; i < li.children.length; i++) {
112
+ if (li.children[i].tagName === 'A') {
113
+ return li.children[i];
114
+ }
115
+ }
116
+ return li.querySelector('a');
117
+ }
118
+
119
+ function getDecodedTail(input) {
120
+ var tail = '';
121
+ try {
122
+ var parsed = new URL(input, window.location.origin);
123
+ tail = (parsed.pathname || '').split('/').pop() || '';
124
+ } catch (e) {
125
+ tail = String(input || '')
126
+ .split('#')[0]
127
+ .split('?')[0]
128
+ .split('/')
129
+ .pop() || '';
130
+ }
131
+ try {
132
+ return decodeURIComponent(tail);
133
+ } catch (e2) {
134
+ return tail;
135
+ }
136
+ }
137
+
138
+ function stripExt(name) {
139
+ return String(name || '').replace(/\.(html|md)$/i, '');
140
+ }
141
+
142
+ // 打开菜单时定位到当前页面,并高亮其父级目录路径
143
+ function locateCurrentMenuItem() {
144
+ var menuContent = menuPopup.querySelector('.menu-popup-content');
145
+ if (!menuContent) {
146
+ return;
147
+ }
148
+
149
+ var previousMarks = menuContent.querySelectorAll(
150
+ '.menu-current-item, .menu-current-link, .menu-ancestor-item'
151
+ );
152
+ previousMarks.forEach(function(node) {
153
+ node.classList.remove('menu-current-item');
154
+ node.classList.remove('menu-current-link');
155
+ node.classList.remove('menu-ancestor-item');
156
+ });
157
+
158
+ var currentLi = menuContent.querySelector('li.active');
159
+ if (!currentLi) {
160
+ // 回退:按当前 URL 匹配菜单项(兼容中文路径编码)
161
+ var currentTail = getDecodedTail(window.location.href);
162
+ var currentBase = stripExt(currentTail);
163
+ if (currentTail) {
164
+ var links = menuContent.querySelectorAll('a[href]');
165
+ for (var i = 0; i < links.length; i++) {
166
+ var linkTail = getDecodedTail(links[i].getAttribute('href'));
167
+ var linkBase = stripExt(linkTail);
168
+ if (linkTail === currentTail || linkBase === currentBase) {
169
+ currentLi = links[i].closest('li');
170
+ break;
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ if (!currentLi) {
177
+ return;
178
+ }
179
+
180
+ currentLi.classList.add('menu-current-item');
181
+ var currentLink = getDirectLink(currentLi);
182
+ if (currentLink) {
183
+ currentLink.classList.add('menu-current-link');
184
+ }
185
+
186
+ var parent = currentLi.parentElement;
187
+ while (parent && parent !== menuPopup) {
188
+ if (parent.tagName === 'LI') {
189
+ parent.classList.add('menu-ancestor-item');
190
+ }
191
+ parent = parent.parentElement;
192
+ }
193
+
194
+ try {
195
+ currentLi.scrollIntoView({
196
+ block: 'center',
197
+ inline: 'nearest'
198
+ });
199
+ } catch (e) {
200
+ // 兼容旧浏览器
201
+ currentLi.scrollIntoView();
202
+ }
203
+ }
106
204
 
107
205
  // 打开菜单
108
206
  function openMenu() {
@@ -110,6 +208,7 @@ style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background:
110
208
  menuPopup.classList.add('menu-popup-show');
111
209
  menuOverlay.classList.add('menu-overlay-show');
112
210
  document.body.style.overflow = 'hidden'; // 防止背景滚动
211
+ setTimeout(locateCurrentMenuItem, 50);
113
212
  }
114
213
 
115
214
  // 关闭菜单
@@ -404,9 +503,277 @@ style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background:
404
503
  setTimeout(initMenuPopup, 500);
405
504
  setTimeout(initMenuPopup, 1000);
406
505
 
407
- // 图片预览功能初始化
506
+ // 图片/mermaid 预览功能初始化
408
507
  (function() {
409
508
  var imageViewer = null;
509
+ var imageViewerObserver = null;
510
+ var viewerRefreshTimer = null;
511
+ var mermaidPreviewModal = null;
512
+ var mermaidPreviewScale = 1;
513
+ var mermaidPreviewOffsetX = 0;
514
+ var mermaidPreviewOffsetY = 0;
515
+
516
+ function applyMermaidPreviewTransform(container) {
517
+ if (!container) {
518
+ return;
519
+ }
520
+ container.style.transform =
521
+ 'translate(' + mermaidPreviewOffsetX + 'px, ' + mermaidPreviewOffsetY + 'px) scale(' + mermaidPreviewScale + ')';
522
+ }
523
+
524
+ function scheduleInitImageViewer(delay) {
525
+ if (viewerRefreshTimer) {
526
+ clearTimeout(viewerRefreshTimer);
527
+ }
528
+ viewerRefreshTimer = setTimeout(function() {
529
+ initImageViewer();
530
+ }, typeof delay === 'number' ? delay : 80);
531
+ }
532
+
533
+ function ensureMermaidPreviewModal() {
534
+ if (mermaidPreviewModal) {
535
+ return mermaidPreviewModal;
536
+ }
537
+ var modal = document.createElement('div');
538
+ modal.id = 'mermaid-preview-modal';
539
+ modal.style.cssText = [
540
+ 'position:fixed',
541
+ 'inset:0',
542
+ 'display:none',
543
+ 'z-index:100010',
544
+ 'background:rgba(12,16,22,0.82)',
545
+ 'align-items:center',
546
+ 'justify-content:center',
547
+ 'padding:24px'
548
+ ].join(';');
549
+
550
+ var container = document.createElement('div');
551
+ container.className = 'mermaid-preview-container';
552
+ container.style.cssText = [
553
+ 'position:relative',
554
+ 'max-width:96vw',
555
+ 'max-height:92vh',
556
+ 'overflow:hidden',
557
+ 'background:#fff',
558
+ 'border-radius:10px',
559
+ 'padding:14px',
560
+ 'box-shadow:0 12px 36px rgba(0,0,0,0.35)',
561
+ 'transform-origin:center center',
562
+ 'transition:transform 0.08s linear'
563
+ ].join(';');
564
+
565
+ var closeBtn = document.createElement('button');
566
+ closeBtn.type = 'button';
567
+ closeBtn.innerHTML = '&times;';
568
+ closeBtn.setAttribute('aria-label', '关闭Mermaid预览');
569
+ closeBtn.style.cssText = [
570
+ 'position:absolute',
571
+ 'right:10px',
572
+ 'top:6px',
573
+ 'width:28px',
574
+ 'height:28px',
575
+ 'border:none',
576
+ 'border-radius:50%',
577
+ 'background:rgba(0,0,0,0.08)',
578
+ 'cursor:pointer',
579
+ 'font-size:20px',
580
+ 'line-height:1'
581
+ ].join(';');
582
+
583
+ var content = document.createElement('div');
584
+ content.className = 'mermaid-preview-content';
585
+ content.style.cssText = [
586
+ 'min-width:320px',
587
+ 'min-height:160px',
588
+ 'width:90vw',
589
+ 'height:82vh',
590
+ 'display:flex',
591
+ 'align-items:center',
592
+ 'justify-content:center',
593
+ 'overflow:hidden',
594
+ 'cursor:default'
595
+ ].join(';');
596
+
597
+ closeBtn.addEventListener('click', function() {
598
+ modal.style.display = 'none';
599
+ document.body.style.overflow = '';
600
+ mermaidPreviewScale = 1;
601
+ mermaidPreviewOffsetX = 0;
602
+ mermaidPreviewOffsetY = 0;
603
+ applyMermaidPreviewTransform(container);
604
+ });
605
+ modal.addEventListener('click', function(e) {
606
+ if (e.target === modal) {
607
+ modal.style.display = 'none';
608
+ document.body.style.overflow = '';
609
+ mermaidPreviewScale = 1;
610
+ mermaidPreviewOffsetX = 0;
611
+ mermaidPreviewOffsetY = 0;
612
+ applyMermaidPreviewTransform(container);
613
+ }
614
+ });
615
+ document.addEventListener('keydown', function(e) {
616
+ if (e.key === 'Escape' && modal.style.display !== 'none') {
617
+ modal.style.display = 'none';
618
+ document.body.style.overflow = '';
619
+ mermaidPreviewScale = 1;
620
+ mermaidPreviewOffsetX = 0;
621
+ mermaidPreviewOffsetY = 0;
622
+ applyMermaidPreviewTransform(container);
623
+ }
624
+ });
625
+
626
+ container.appendChild(closeBtn);
627
+ container.appendChild(content);
628
+ modal.appendChild(container);
629
+ document.body.appendChild(modal);
630
+ mermaidPreviewModal = modal;
631
+ return mermaidPreviewModal;
632
+ }
633
+
634
+ function bindMermaidPreview(markdownBody) {
635
+ var mermaidSvgs = markdownBody.querySelectorAll('.mermaid svg');
636
+ mermaidSvgs.forEach(function(svg) {
637
+ if (svg.getAttribute('data-mermaid-preview-bound') === 'true') {
638
+ return;
639
+ }
640
+ svg.setAttribute('data-mermaid-preview-bound', 'true');
641
+ svg.style.cursor = 'zoom-in';
642
+ svg.addEventListener('click', function(e) {
643
+ e.preventDefault();
644
+ e.stopPropagation();
645
+
646
+ var modal = ensureMermaidPreviewModal();
647
+ var content = modal.querySelector('.mermaid-preview-content');
648
+ var container = modal.querySelector('.mermaid-preview-container');
649
+ if (!content || !container) {
650
+ return;
651
+ }
652
+ content.innerHTML = '';
653
+
654
+ var clone = svg.cloneNode(true);
655
+ var vb = clone.getAttribute('viewBox');
656
+ var vbWidth = 0;
657
+ var vbHeight = 0;
658
+ if (vb) {
659
+ var parts = vb.trim().split(/\s+/);
660
+ if (parts.length === 4) {
661
+ vbWidth = parseFloat(parts[2]) || 0;
662
+ vbHeight = parseFloat(parts[3]) || 0;
663
+ }
664
+ }
665
+ if (!vbWidth || !vbHeight) {
666
+ var rect = svg.getBoundingClientRect();
667
+ vbWidth = rect.width || 800;
668
+ vbHeight = rect.height || 500;
669
+ }
670
+
671
+ var maxWidth = Math.floor(window.innerWidth * 0.9);
672
+ var maxHeight = Math.floor(window.innerHeight * 0.82);
673
+ var fitScale = Math.min(maxWidth / vbWidth, maxHeight / vbHeight);
674
+ if (!isFinite(fitScale) || fitScale <= 0) {
675
+ fitScale = 1;
676
+ }
677
+ // 预览默认尽量放大显示,但保留比例,避免文字过小
678
+ var defaultScale = Math.max(fitScale, 1.25);
679
+ var renderWidth = Math.round(vbWidth * defaultScale);
680
+ var renderHeight = Math.round(vbHeight * defaultScale);
681
+ if (renderWidth > maxWidth || renderHeight > maxHeight) {
682
+ var clampScale = Math.min(maxWidth / vbWidth, maxHeight / vbHeight);
683
+ renderWidth = Math.round(vbWidth * clampScale);
684
+ renderHeight = Math.round(vbHeight * clampScale);
685
+ }
686
+
687
+ clone.removeAttribute('width');
688
+ clone.removeAttribute('height');
689
+ clone.style.width = Math.max(320, renderWidth) + 'px';
690
+ clone.style.height = 'auto';
691
+ clone.style.maxWidth = '100%';
692
+ clone.style.maxHeight = '100%';
693
+ clone.style.display = 'block';
694
+ clone.style.cursor = 'grab';
695
+ clone.style.userSelect = 'none';
696
+ content.appendChild(clone);
697
+
698
+ // 改为缩放整个预览容器,不出现滚动条
699
+ if (content.__wheelZoomHandler) {
700
+ content.removeEventListener('wheel', content.__wheelZoomHandler, false);
701
+ }
702
+ var wheelZoomHandler = function(evt) {
703
+ evt.preventDefault();
704
+ var zoomStep = evt.deltaY < 0 ? 1.08 : 0.92;
705
+ mermaidPreviewScale = Math.max(0.8, Math.min(2.2, mermaidPreviewScale * zoomStep));
706
+ applyMermaidPreviewTransform(container);
707
+ };
708
+ content.addEventListener('wheel', wheelZoomHandler, { passive: false });
709
+ content.__wheelZoomHandler = wheelZoomHandler;
710
+
711
+ // 支持按住左键拖动(小手光标)
712
+ content.style.cursor = 'grab';
713
+ var isDragging = false;
714
+ var dragStartX = 0;
715
+ var dragStartY = 0;
716
+ var dragBaseX = 0;
717
+ var dragBaseY = 0;
718
+
719
+ if (content.__dragMouseDownHandler) {
720
+ content.removeEventListener('mousedown', content.__dragMouseDownHandler, false);
721
+ }
722
+ if (content.__dragMouseMoveHandler) {
723
+ window.removeEventListener('mousemove', content.__dragMouseMoveHandler, false);
724
+ }
725
+ if (content.__dragMouseUpHandler) {
726
+ window.removeEventListener('mouseup', content.__dragMouseUpHandler, false);
727
+ }
728
+
729
+ var dragMouseDownHandler = function(evt) {
730
+ if (evt.button !== 0) {
731
+ return;
732
+ }
733
+ isDragging = true;
734
+ dragStartX = evt.clientX;
735
+ dragStartY = evt.clientY;
736
+ dragBaseX = mermaidPreviewOffsetX;
737
+ dragBaseY = mermaidPreviewOffsetY;
738
+ content.style.cursor = 'grabbing';
739
+ clone.style.cursor = 'grabbing';
740
+ container.style.transition = 'none';
741
+ evt.preventDefault();
742
+ };
743
+ var dragMouseMoveHandler = function(evt) {
744
+ if (!isDragging) {
745
+ return;
746
+ }
747
+ mermaidPreviewOffsetX = dragBaseX + (evt.clientX - dragStartX);
748
+ mermaidPreviewOffsetY = dragBaseY + (evt.clientY - dragStartY);
749
+ applyMermaidPreviewTransform(container);
750
+ };
751
+ var dragMouseUpHandler = function() {
752
+ if (!isDragging) {
753
+ return;
754
+ }
755
+ isDragging = false;
756
+ content.style.cursor = 'grab';
757
+ clone.style.cursor = 'grab';
758
+ container.style.transition = 'transform 0.08s linear';
759
+ };
760
+
761
+ content.addEventListener('mousedown', dragMouseDownHandler, false);
762
+ window.addEventListener('mousemove', dragMouseMoveHandler, false);
763
+ window.addEventListener('mouseup', dragMouseUpHandler, false);
764
+ content.__dragMouseDownHandler = dragMouseDownHandler;
765
+ content.__dragMouseMoveHandler = dragMouseMoveHandler;
766
+ content.__dragMouseUpHandler = dragMouseUpHandler;
767
+
768
+ modal.style.display = 'flex';
769
+ document.body.style.overflow = 'hidden';
770
+ mermaidPreviewScale = 1;
771
+ mermaidPreviewOffsetX = 0;
772
+ mermaidPreviewOffsetY = 0;
773
+ applyMermaidPreviewTransform(container);
774
+ });
775
+ });
776
+ }
410
777
 
411
778
  function initImageViewer() {
412
779
  // 查找markdown-body容器
@@ -414,6 +781,20 @@ style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background:
414
781
  if (!markdownBody) {
415
782
  return;
416
783
  }
784
+
785
+ // 监听内容变化:mermaid 常常是异步渲染 SVG
786
+ if (!imageViewerObserver) {
787
+ imageViewerObserver = new MutationObserver(function() {
788
+ scheduleInitImageViewer(80);
789
+ });
790
+ imageViewerObserver.observe(markdownBody, {
791
+ childList: true,
792
+ subtree: true
793
+ });
794
+ }
795
+
796
+ // Mermaid 保持原始 SVG 渲染,点击时弹出预览(避免转图片导致文字裁切)
797
+ bindMermaidPreview(markdownBody);
417
798
 
418
799
  // 查找所有图片
419
800
  var images = markdownBody.querySelectorAll('img');
@@ -433,7 +814,7 @@ style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background:
433
814
  // 为图片添加样式和点击事件
434
815
  images.forEach(function(img) {
435
816
  // 跳过已经是链接内的图片
436
- if (img.parentElement.tagName === 'A') {
817
+ if (img.parentElement && img.parentElement.tagName === 'A') {
437
818
  return;
438
819
  }
439
820
 
@@ -446,11 +827,41 @@ style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background:
446
827
  try {
447
828
  imageViewer = new Viewer(markdownBody, {
448
829
  inline: false,
449
- viewed: function() {
450
- // 查看图片时的回调
451
- },
452
- hidden: function() {
453
- // 关闭查看器时的回调
830
+ viewed: function(event) {
831
+ // 仅对 Mermaid 图应用更大的默认缩放和白底,提升文字可读性
832
+ try {
833
+ var originalImage = event && event.detail && event.detail.originalImage;
834
+ var isMermaidImage = !!(
835
+ originalImage &&
836
+ originalImage.classList &&
837
+ originalImage.classList.contains('mermaid-preview-image')
838
+ );
839
+ if (!isMermaidImage || !imageViewer) {
840
+ return;
841
+ }
842
+
843
+ var imageData = imageViewer.imageData;
844
+ var viewerData = imageViewer.viewerData;
845
+ if (!imageData || !viewerData || !imageData.naturalWidth || !imageData.naturalHeight) {
846
+ return;
847
+ }
848
+
849
+ var fitRatio = Math.min(
850
+ viewerData.width / imageData.naturalWidth,
851
+ viewerData.height / imageData.naturalHeight
852
+ );
853
+ var targetRatio = Math.max(fitRatio * 1.2, 1.25);
854
+ imageViewer.zoomTo(targetRatio);
855
+
856
+ if (imageViewer.image) {
857
+ imageViewer.image.style.backgroundColor = '#ffffff';
858
+ imageViewer.image.style.padding = '14px';
859
+ imageViewer.image.style.borderRadius = '8px';
860
+ imageViewer.image.style.boxShadow = '0 6px 24px rgba(0, 0, 0, 0.25)';
861
+ }
862
+ } catch (e) {
863
+ console.error('Mermaid 预览缩放初始化失败:', e);
864
+ }
454
865
  },
455
866
  toolbar: {
456
867
  zoomIn: true,
@@ -484,6 +895,8 @@ style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background:
484
895
  // 延迟初始化(处理动态显示的情况)
485
896
  setTimeout(initImageViewer, 500);
486
897
  setTimeout(initImageViewer, 1000);
898
+ setTimeout(initImageViewer, 2000);
899
+ setTimeout(initImageViewer, 3500);
487
900
  })();
488
901
 
489
902
  // 上一节/下一节导航功能
@@ -30,6 +30,25 @@
30
30
  cursor: zoom-in
31
31
  &:hover
32
32
  opacity: 0.9
33
+ &.mermaid-preview-image
34
+ background: #fff
35
+ padding: 10px
36
+ border: 1px solid #e5e7eb
37
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08)
38
+ .mermaid
39
+ display: flex
40
+ justify-content: center
41
+ overflow-x: auto
42
+ overflow-y: hidden
43
+ padding: 8px 0
44
+ background: #fff
45
+ border-radius: 6px
46
+
47
+ svg
48
+ width: 130% !important
49
+ height: auto !important
50
+ min-width: 1100px
51
+ max-width: none !important
33
52
  a
34
53
  color: #0969da
35
54
  text-decoration: none
@@ -167,6 +167,17 @@ $hover-color = #1ABC9C // 悬停绿色
167
167
  background: rgba($primary-color, 0.1)
168
168
  color: $primary-color
169
169
  font-weight: 600
170
+ &.menu-ancestor-item
171
+ > a
172
+ background: rgba($primary-color, 0.06)
173
+ color: darken($primary-color, 10%)
174
+ &.menu-current-item
175
+ > a,
176
+ > a.menu-current-link
177
+ background: rgba($primary-color, 0.14)
178
+ color: $primary-color
179
+ font-weight: 700
180
+ border-left: 3px solid $primary-color
170
181
  ul
171
182
  margin-left: 20px
172
183
  margin-top: 5px