docnest 0.1.12 → 0.1.14

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 CHANGED
@@ -78,6 +78,8 @@ export default defineConfig({
78
78
  auth: {
79
79
  enabled: true,
80
80
  passphrase: '请替换为项目自己的长口令',
81
+ // 服务通过反向代理挂载在子路径时填写,例如 /documents;默认跳转到 /。
82
+ redirectPrefix: '',
81
83
  stateFile: '.docnest/auth.json',
82
84
  localStorageKey: 'my-project:auth-passphrase',
83
85
  sessionTtlMinutes: 24 * 60,
@@ -91,6 +93,8 @@ export default defineConfig({
91
93
  `localStorageKey` 可选,用于为不同宿主项目隔离浏览器存储键。内置授权不提供“修改口令”或“退出”按钮;需要更换口令时,停止服务、删除 `stateFile`,修改配置中的 `passphrase` 后重新启动。
92
94
  建议把 `.docnest/` 加入宿主项目的 `.gitignore`,并为口令状态文件设置仅当前用户可读写的权限。
93
95
 
96
+ 如果 DocNest 由反向代理挂载在 `/documents/` 等子路径下,将 `auth.redirectPrefix` 配置为 `/documents`。登录页会把原始文档目标写成带此前缀的路径,口令登录后的服务端跳转也会使用同一前缀;不配置时保持跳转到 `/` 的默认行为。
97
+
94
98
  授权开启但既没有状态文件,也没有初始 `passphrase` 时,DocNest 会拒绝启动,避免意外以空口令开放文档。
95
99
 
96
100
  如需显式指定端口、文档目录或监听地址:
@@ -136,3 +140,16 @@ npm run pack:check
136
140
  ```
137
141
 
138
142
  最低支持 Node.js 20,验证 Node.js 20 和 22。
143
+
144
+ ## 发布规范
145
+
146
+ DocNest 的 GitHub Release 和 npm 发布统一由 GitHub Actions 完成,不在本地直接执行 `npm publish`。
147
+
148
+ 发布新版本时:
149
+
150
+ 1. 更新 `package.json` 和 `package-lock.json` 的版本号,并运行 `npm test` 与 `npm run pack:check`。
151
+ 2. 将版本提交推送到 `main`。
152
+ 3. 在 `main` 上创建与包版本一致的标签,例如 `v0.1.13`。
153
+ 4. 推送该标签:`git push origin v0.1.13`。
154
+
155
+ 标签推送后,`.github/workflows/publish.yml` 会校验标签与包版本一致,使用 npm OIDC 发布公开包,并自动创建名为 `DocNest v0.1.13` 的 GitHub Release。远程仓库只保留 `main` 分支,发布标签不需要推送临时开发分支。
package/dist/config.d.ts CHANGED
@@ -28,6 +28,11 @@ export type DocNestConfig = {
28
28
  auth?: {
29
29
  enabled?: boolean;
30
30
  passphrase?: string;
31
+ /**
32
+ * Prefix to add to authentication return targets when the service is
33
+ * mounted below a reverse-proxy path, for example `/documents`.
34
+ */
35
+ redirectPrefix?: string;
31
36
  stateFile?: string;
32
37
  localStorageKey?: string;
33
38
  sessionTtlMinutes?: number;
@@ -49,6 +54,7 @@ export type ResolvedDocNestConfig = Required<DocNestConfig> & {
49
54
  auth: {
50
55
  enabled: boolean;
51
56
  passphrase: string;
57
+ redirectPrefix: string;
52
58
  stateFile: string;
53
59
  localStorageKey: string;
54
60
  sessionTtlMinutes: number;
package/dist/config.js CHANGED
@@ -45,6 +45,20 @@ function normalizePositiveNumber(value, fallback) {
45
45
  const normalized = Number(value);
46
46
  return Number.isFinite(normalized) && normalized > 0 ? normalized : fallback;
47
47
  }
48
+ function normalizeRedirectPrefix(value) {
49
+ if (typeof value !== 'string')
50
+ return '';
51
+ const normalized = value.trim();
52
+ if (!normalized ||
53
+ normalized === '/' ||
54
+ !normalized.startsWith('/') ||
55
+ normalized.startsWith('//') ||
56
+ normalized.includes('//') ||
57
+ normalized.includes('\\') ||
58
+ /[\r\n?#]/.test(normalized))
59
+ return '';
60
+ return normalized.replace(/\/+$/, '') || '';
61
+ }
48
62
  function normalizeThemeName(value) {
49
63
  if (value === 'current-docs')
50
64
  return 'slate-modern';
@@ -113,6 +127,7 @@ async function loadConfig(projectRoot = process.cwd()) {
113
127
  auth: {
114
128
  enabled: auth.enabled ?? false,
115
129
  passphrase: typeof auth.passphrase === 'string' ? auth.passphrase : '',
130
+ redirectPrefix: normalizeRedirectPrefix(auth.redirectPrefix),
116
131
  stateFile: node_path_1.default.resolve(projectRoot, auth.stateFile?.trim() || '.docnest/auth.json'),
117
132
  localStorageKey: authLocalStorageKey,
118
133
  sessionTtlMinutes: normalizePositiveNumber(auth.sessionTtlMinutes, 24 * 60),
package/dist/server.js CHANGED
@@ -280,6 +280,17 @@ function safeNextPath(value) {
280
280
  return '/';
281
281
  return value;
282
282
  }
283
+ function authRedirectTarget(value) {
284
+ const nextPath = safeNextPath(value);
285
+ const prefix = typeof AUTH_CONFIG.redirectPrefix === 'string' ? AUTH_CONFIG.redirectPrefix : '';
286
+ if (!prefix ||
287
+ nextPath === prefix ||
288
+ nextPath.startsWith(`${prefix}/`) ||
289
+ nextPath.startsWith(`${prefix}?`) ||
290
+ nextPath.startsWith(`${prefix}#`))
291
+ return nextPath;
292
+ return nextPath === '/' ? `${prefix}/` : `${prefix}${nextPath}`;
293
+ }
283
294
  function setAuthCookie(res, token) {
284
295
  const maxAge = AUTH_MANAGER.sessionMaxAgeSeconds;
285
296
  res.setHeader('Set-Cookie', `${AUTH_MANAGER.sessionCookieName}=${encodeURIComponent(token)}; Max-Age=${maxAge}; Path=/; HttpOnly; SameSite=Lax`);
@@ -292,7 +303,7 @@ function wantsJson(req) {
292
303
  }
293
304
  function renderLoginPage(res, nextPath, error, status = 200) {
294
305
  return res.status(status).render('login', {
295
- nextPath: safeNextPath(nextPath),
306
+ nextPath: authRedirectTarget(nextPath),
296
307
  error: error || '',
297
308
  assetVersion: ASSET_VERSION,
298
309
  });
@@ -316,7 +327,7 @@ app.post('/auth/verify', (req, res) => {
316
327
  app.post('/auth/login', (req, res) => {
317
328
  if (!BUILT_IN_AUTH_ENABLED)
318
329
  return res.redirect('/');
319
- const nextPath = safeNextPath(req.body?.next);
330
+ const nextPath = authRedirectTarget(req.body?.next);
320
331
  const token = AUTH_MANAGER.createSession(String(req.body?.passphrase || ''));
321
332
  if (!token)
322
333
  return renderLoginPage(res, nextPath, '口令不正确,请重试。', 401);
@@ -36,6 +36,8 @@ export default defineConfig({
36
36
  enabled: false,
37
37
  // 仅首次启动且授权状态文件不存在时使用;更换口令需手动删除状态文件后重启。
38
38
  passphrase: '',
39
+ // 通过反向代理挂载在子路径时,为登录成功后的目标补此前缀;默认空值跳转到根路径。
40
+ redirectPrefix: '',
39
41
  stateFile: '.docnest/auth.json',
40
42
  // 前端 localStorage 的键;口令会永久保存在浏览器本地,直到手动清除或口令失效。
41
43
  localStorageKey: 'my-project:auth-passphrase',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docnest",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "可复用的本地 Markdown 文档中心",
5
5
  "repository": {
6
6
  "type": "git",
@@ -522,6 +522,7 @@ body[data-theme="dark"] .dir-toggle.active {
522
522
  /* 主内容区 - 白底、留白 */
523
523
  .main-content {
524
524
  flex: 1;
525
+ min-width: 0;
525
526
  overflow-y: auto;
526
527
  overflow-x: hidden;
527
528
  padding: 2rem 2.5rem;
@@ -535,6 +536,7 @@ body[data-theme="dark"] .dir-toggle.active {
535
536
  align-items: flex-start;
536
537
  gap: 24px;
537
538
  width: min(1400px, 100%);
539
+ min-width: 0;
538
540
  margin: 0 auto;
539
541
  }
540
542
 
@@ -1032,7 +1034,8 @@ body[data-theme="dark"] .dir-toggle.active {
1032
1034
  /* Markdown 内容 - 限宽、舒适行距;普通显示器上更宽,超宽屏用 92% 避免单行过长 */
1033
1035
  .markdown-body {
1034
1036
  flex: 1;
1035
- width: min(1024px, 100%);
1037
+ min-width: 0;
1038
+ width: min(var(--doc-reading-width, 1024px), 100%);
1036
1039
  max-width: 100%;
1037
1040
  margin: 0;
1038
1041
  box-sizing: border-box;
@@ -1050,7 +1053,7 @@ body[data-theme="dark"] .dir-toggle.active {
1050
1053
  }
1051
1054
 
1052
1055
  .doc-layout {
1053
- width: min(1024px, 100%);
1056
+ width: min(var(--doc-reading-width, 1024px), 100%);
1054
1057
  }
1055
1058
  }
1056
1059
 
@@ -1254,6 +1257,9 @@ body[data-theme="dark"] .not-found-path {
1254
1257
  text-align: center;
1255
1258
  background: var(--linear-bg-subtle);
1256
1259
  padding: 16px;
1260
+ min-width: 0;
1261
+ max-width: 100%;
1262
+ box-sizing: border-box;
1257
1263
  border-radius: var(--linear-radius);
1258
1264
  border: 1px solid var(--linear-border);
1259
1265
  overflow-x: auto;
@@ -1301,9 +1307,12 @@ body[data-theme="dark"] .markdown-body .mermaid::after {
1301
1307
 
1302
1308
  /* 确保 Mermaid SVG 正确显示 */
1303
1309
  .markdown-body .mermaid svg {
1304
- max-width: 100%;
1305
- height: auto;
1310
+ width: 100% !important;
1311
+ max-width: 100% !important;
1312
+ min-width: 0 !important;
1313
+ height: auto !important;
1306
1314
  display: block;
1315
+ margin: 0 auto;
1307
1316
  }
1308
1317
 
1309
1318
  /* Mermaid 下载按钮 */
@@ -2079,7 +2088,7 @@ body[data-theme="dark"] .tree-nav::-webkit-scrollbar-thumb:hover {
2079
2088
  .markdown-body .mermaid svg {
2080
2089
  max-width: 100% !important;
2081
2090
  max-height: 75vh !important; /* 高流程图缩放到单页内 */
2082
- width: auto !important;
2091
+ width: 100% !important;
2083
2092
  height: auto !important;
2084
2093
  display: block !important;
2085
2094
  margin: 0 auto !important;
@@ -2,8 +2,9 @@
2
2
  * DocNest theme profiles
3
3
  *
4
4
  * One semantic DOM, five visual systems. The original DocNest rules remain
5
- * the Slate Modern baseline; this file owns theme tokens and deliberate layout
6
- * variations without forking templates or client behavior.
5
+ * the Slate Modern baseline for page geometry (main-content padding,
6
+ * doc-layout max-width/gap, markdown reading width). This file owns theme
7
+ * tokens and visual character without forking templates or client behavior.
7
8
  */
8
9
 
9
10
  :root {
@@ -19,6 +20,7 @@
19
20
  --doc-code-bg: #f6f7f8;
20
21
  --doc-code-text: #24262b;
21
22
  --doc-header-height: auto;
23
+ /* Shared with Slate Modern; theme profiles must not override page geometry. */
22
24
  --doc-reading-width: 1024px;
23
25
  }
24
26
 
@@ -405,7 +407,6 @@ body[data-doc-theme="editorial-atlas"] {
405
407
  "Microsoft YaHei", sans-serif;
406
408
  --doc-font-reading: "Songti SC", SimSun, "Noto Serif CJK SC", serif;
407
409
  --doc-font-display: "Songti SC", SimSun, "Noto Serif CJK SC", serif;
408
- --doc-reading-width: 860px;
409
410
  }
410
411
 
411
412
  body[data-doc-theme="editorial-atlas"][data-theme="dark"] {
@@ -452,18 +453,7 @@ body[data-doc-theme="editorial-atlas"] .sidebar {
452
453
  border-right-color: var(--doc-rule-strong);
453
454
  }
454
455
 
455
- body[data-doc-theme="editorial-atlas"] .main-content {
456
- padding: clamp(2rem, 4vw, 4.75rem) clamp(1.5rem, 5vw, 5.5rem);
457
- background: var(--linear-bg);
458
- }
459
-
460
- body[data-doc-theme="editorial-atlas"] .doc-layout {
461
- width: min(1240px, 100%);
462
- gap: 42px;
463
- }
464
-
465
456
  body[data-doc-theme="editorial-atlas"] .markdown-body {
466
- width: min(var(--doc-reading-width), 100%);
467
457
  background: transparent;
468
458
  font-size: 1rem;
469
459
  line-height: 1.9;
@@ -520,7 +510,6 @@ body[data-doc-theme="precision-index"] {
520
510
  "Microsoft YaHei", sans-serif;
521
511
  --doc-font-reading: var(--doc-font-ui);
522
512
  --doc-font-display: var(--doc-font-ui);
523
- --doc-reading-width: 980px;
524
513
  }
525
514
 
526
515
  body[data-doc-theme="precision-index"][data-theme="dark"] {
@@ -597,28 +586,17 @@ body[data-doc-theme="precision-index"] .file-link.active {
597
586
  background: var(--doc-surface-raised);
598
587
  }
599
588
 
600
- body[data-doc-theme="precision-index"] .main-content {
601
- padding: 0;
602
- background: var(--linear-bg);
603
- }
604
-
605
589
  body[data-doc-theme="precision-index"] .breadcrumbs {
606
- margin: 0;
607
- padding: 0.75rem 1.5rem;
608
590
  border-bottom: 1px solid var(--doc-rule-strong);
609
591
  background: var(--linear-bg-subtle);
610
592
  font-size: 0.6875rem;
611
593
  }
612
594
 
613
595
  body[data-doc-theme="precision-index"] .doc-layout {
614
- width: 100%;
615
- gap: 0;
616
596
  background: var(--doc-surface-raised);
617
597
  }
618
598
 
619
599
  body[data-doc-theme="precision-index"] .markdown-body {
620
- width: min(var(--doc-reading-width), 100%);
621
- padding: clamp(2.5rem, 5vw, 5rem);
622
600
  border-right: 1px solid var(--doc-rule-strong);
623
601
  font-size: 0.9rem;
624
602
  line-height: 1.75;
@@ -651,11 +629,6 @@ body[data-doc-theme="precision-index"] .markdown-body h2 {
651
629
  }
652
630
 
653
631
  body[data-doc-theme="precision-index"] .doc-toc {
654
- top: 0;
655
- width: 216px;
656
- flex-basis: 216px;
657
- max-height: calc(100vh - 54px);
658
- padding: 1.25rem;
659
632
  border: 0;
660
633
  border-radius: 0;
661
634
  background: var(--linear-bg-subtle);
@@ -666,12 +639,6 @@ body[data-doc-theme="precision-index"] .doc-toc-title {
666
639
  text-transform: uppercase;
667
640
  }
668
641
 
669
- body[data-doc-theme="precision-index"] .dir-listing-page,
670
- body[data-doc-theme="precision-index"] .welcome {
671
- margin: 0;
672
- padding: clamp(2rem, 5vw, 5rem);
673
- }
674
-
675
642
  /* Theme 04: Archive Room */
676
643
  body[data-doc-theme="archive-room"] {
677
644
  --linear-bg: #f3efe4;
@@ -693,7 +660,6 @@ body[data-doc-theme="archive-room"] {
693
660
  "Microsoft YaHei", sans-serif;
694
661
  --doc-font-reading: "Songti SC", SimSun, "Noto Serif CJK SC", serif;
695
662
  --doc-font-display: "Songti SC", SimSun, "Noto Serif CJK SC", serif;
696
- --doc-reading-width: 900px;
697
663
  }
698
664
 
699
665
  body[data-doc-theme="archive-room"][data-theme="dark"] {
@@ -761,7 +727,6 @@ body[data-doc-theme="archive-room"][data-theme="dark"] .file-link.active {
761
727
  }
762
728
 
763
729
  body[data-doc-theme="archive-room"] .main-content {
764
- padding: clamp(2rem, 4vw, 4.5rem);
765
730
  background-color: var(--linear-bg);
766
731
  background-image:
767
732
  linear-gradient(rgba(93, 96, 85, 0.045) 1px, transparent 1px),
@@ -769,14 +734,8 @@ body[data-doc-theme="archive-room"] .main-content {
769
734
  background-size: 24px 24px;
770
735
  }
771
736
 
772
- body[data-doc-theme="archive-room"] .doc-layout {
773
- width: min(1280px, 100%);
774
- gap: 34px;
775
- }
776
-
777
737
  body[data-doc-theme="archive-room"] .markdown-body {
778
- width: min(var(--doc-reading-width), 100%);
779
- padding: clamp(1.5rem, 3vw, 3rem);
738
+ padding: 1.5rem 1.5rem 3rem;
780
739
  border: 1px solid var(--doc-rule-strong);
781
740
  background: var(--doc-surface-raised);
782
741
  box-shadow: 7px 7px 0 var(--linear-border);
@@ -836,7 +795,6 @@ body[data-doc-theme="swiss-manual"] {
836
795
  "Microsoft YaHei", sans-serif;
837
796
  --doc-font-reading: var(--doc-font-ui);
838
797
  --doc-font-display: var(--doc-font-ui);
839
- --doc-reading-width: 960px;
840
798
  }
841
799
 
842
800
  body[data-doc-theme="swiss-manual"][data-theme="dark"] {
@@ -939,27 +897,13 @@ body[data-doc-theme="swiss-manual"] .file-link.active {
939
897
  font-weight: 800;
940
898
  }
941
899
 
942
- body[data-doc-theme="swiss-manual"] .main-content {
943
- padding: 0;
944
- background: var(--doc-surface-raised);
945
- }
946
-
947
900
  body[data-doc-theme="swiss-manual"] .breadcrumbs {
948
- margin: 0;
949
- padding: 0.85rem 1.5rem;
950
901
  border-bottom: 1px solid var(--doc-rule-strong);
951
902
  background: var(--linear-bg-subtle);
952
903
  font-size: 0.65rem;
953
904
  }
954
905
 
955
- body[data-doc-theme="swiss-manual"] .doc-layout {
956
- width: 100%;
957
- gap: 0;
958
- }
959
-
960
906
  body[data-doc-theme="swiss-manual"] .markdown-body {
961
- width: min(var(--doc-reading-width), 100%);
962
- padding: clamp(2.5rem, 6vw, 6rem);
963
907
  border-right: 1px solid var(--doc-rule-strong);
964
908
  font-size: 0.95rem;
965
909
  line-height: 1.72;
@@ -1000,22 +944,11 @@ body[data-doc-theme="swiss-manual"] .markdown-body blockquote {
1000
944
  }
1001
945
 
1002
946
  body[data-doc-theme="swiss-manual"] .doc-toc {
1003
- top: 0;
1004
- width: 220px;
1005
- flex-basis: 220px;
1006
- max-height: calc(100vh - 72px);
1007
- padding: 1.25rem;
1008
947
  border: 0;
1009
948
  border-radius: 0;
1010
949
  background: var(--linear-bg-subtle);
1011
950
  }
1012
951
 
1013
- body[data-doc-theme="swiss-manual"] .dir-listing-page,
1014
- body[data-doc-theme="swiss-manual"] .welcome {
1015
- margin: 0;
1016
- padding: clamp(2rem, 6vw, 6rem);
1017
- }
1018
-
1019
952
  /* Shared component refinements for the four independent profiles. */
1020
953
  body:not([data-doc-theme="slate-modern"]) .markdown-body code:not(pre code) {
1021
954
  border: 1px solid var(--linear-border);
@@ -1206,26 +1139,11 @@ body[data-doc-theme="swiss-manual"].not-found-page .deep-space-404__code {
1206
1139
  overflow-y: auto;
1207
1140
  }
1208
1141
 
1209
- body[data-doc-theme="editorial-atlas"] .main-content,
1210
- body[data-doc-theme="archive-room"] .main-content {
1211
- padding: 1.25rem;
1212
- }
1213
-
1214
1142
  body[data-doc-theme="archive-room"] .markdown-body {
1215
1143
  padding: 1.25rem;
1216
1144
  box-shadow: 4px 4px 0 var(--linear-border);
1217
1145
  }
1218
1146
 
1219
- body[data-doc-theme="precision-index"] .main-content,
1220
- body[data-doc-theme="swiss-manual"] .main-content {
1221
- padding: 0;
1222
- }
1223
-
1224
- body[data-doc-theme="precision-index"] .markdown-body,
1225
- body[data-doc-theme="swiss-manual"] .markdown-body {
1226
- padding: 1.25rem;
1227
- }
1228
-
1229
1147
  body[data-doc-theme="precision-index"] .markdown-body h1,
1230
1148
  body[data-doc-theme="swiss-manual"] .markdown-body h1 {
1231
1149
  font-size: clamp(2rem, 12vw, 3.25rem);
@@ -8,6 +8,41 @@ function isDocNestRestrictedMode() {
8
8
  return !!(window.DOCNEST_CONFIG && window.DOCNEST_CONFIG.restrictedMode === true);
9
9
  }
10
10
 
11
+ function parseSvgAbsoluteDimension(value) {
12
+ if (typeof value !== 'string' || !/^\s*\d*\.?\d+(?:e[+-]?\d+)?(?:px)?\s*$/i.test(value)) {
13
+ return null;
14
+ }
15
+
16
+ const dimension = Number.parseFloat(value);
17
+ return Number.isFinite(dimension) && dimension > 0 ? dimension : null;
18
+ }
19
+
20
+ function getDiagramIntrinsicDimensions(svgElement) {
21
+ const viewBox = svgElement.getAttribute('viewBox');
22
+ if (viewBox) {
23
+ const parts = viewBox.trim().split(/[\s,]+/).map(Number);
24
+ const viewBoxWidth = parts[2];
25
+ const viewBoxHeight = parts[3];
26
+ if (
27
+ parts.length === 4 &&
28
+ parts.every(Number.isFinite) &&
29
+ viewBoxWidth > 0 &&
30
+ viewBoxHeight > 0
31
+ ) {
32
+ // viewBox 是图表的实际坐标范围,优先于 Mermaid 的 width="100%"。
33
+ return { width: viewBoxWidth, height: viewBoxHeight };
34
+ }
35
+ }
36
+
37
+ const width = parseSvgAbsoluteDimension(svgElement.getAttribute('width'));
38
+ const height = parseSvgAbsoluteDimension(svgElement.getAttribute('height'));
39
+ if (width && height) return { width, height };
40
+ if (width) return { width, height: width * 0.75 };
41
+ if (height) return { width: height * (4 / 3), height };
42
+
43
+ return { width: 800, height: 600 };
44
+ }
45
+
11
46
  function setupDiagramViewer() {
12
47
  if (isDocNestRestrictedMode()) return;
13
48
  const diagrams = document.querySelectorAll('.markdown-body .mermaid');
@@ -49,39 +84,16 @@ function openDiagramViewer(svgElement) {
49
84
 
50
85
  if (!viewer || !container) return;
51
86
 
52
- // 获取原始 SVG 的尺寸
53
- let originalWidth = parseFloat(svgElement.getAttribute('width'));
54
- let originalHeight = parseFloat(svgElement.getAttribute('height'));
55
-
56
- // 如果没有 width/height,尝试从 viewBox 获取
57
- if (!originalWidth || !originalHeight || isNaN(originalWidth) || isNaN(originalHeight)) {
58
- const viewBox = svgElement.getAttribute('viewBox');
59
- if (viewBox) {
60
- const parts = viewBox.split(/\s+/);
61
- if (parts.length >= 4) {
62
- originalWidth = parseFloat(parts[2]) || 800;
63
- originalHeight = parseFloat(parts[3]) || 600;
64
- }
65
- }
66
- }
67
-
68
- // 如果还是没有,使用默认值
69
- if (!originalWidth || !originalHeight || isNaN(originalWidth) || isNaN(originalHeight)) {
70
- originalWidth = 800;
71
- originalHeight = 600;
72
- }
87
+ // viewBox 才是图表的实际尺寸;不能把 Mermaid 的 width="100%" 解析成 100。
88
+ const { width: originalWidth, height: originalHeight } = getDiagramIntrinsicDimensions(svgElement);
73
89
 
74
90
  // 设置 SVG 样式,确保充分利用空间
75
91
  svgElement.setAttribute('class', 'diagram-viewer-svg');
76
92
  // 清除之前的样式,但保留必要的属性
77
93
  svgElement.removeAttribute('style');
78
- // 确保 SVG 有正确的属性
79
- if (!svgElement.getAttribute('width')) {
80
- svgElement.setAttribute('width', originalWidth);
81
- }
82
- if (!svgElement.getAttribute('height')) {
83
- svgElement.setAttribute('height', originalHeight);
84
- }
94
+ // 查看器需要固定的像素基准;内嵌图仍然保持响应式,不会被这个克隆影响。
95
+ svgElement.setAttribute('width', originalWidth);
96
+ svgElement.setAttribute('height', originalHeight);
85
97
 
86
98
  // 包装元素本身还包含边界 padding,因此适配比例必须把容器 padding 和图表边界一并算入。
87
99
  const padding = 40;
@@ -205,9 +217,8 @@ function resetDiagramZoom() {
205
217
  const wrapper = container.querySelector('.diagram-viewer-svg-wrapper');
206
218
  if (!wrapper) return;
207
219
 
208
- // 获取原始尺寸
209
- const originalWidth = parseFloat(currentSvg.style.width) || parseFloat(currentSvg.getAttribute('width')) || 800;
210
- const originalHeight = parseFloat(currentSvg.style.height) || parseFloat(currentSvg.getAttribute('height')) || 600;
220
+ // 始终从统一的尺寸解析逻辑获取原始尺寸,避免缩放/重置再次误读百分比宽度。
221
+ const { width: originalWidth, height: originalHeight } = getDiagramIntrinsicDimensions(currentSvg);
211
222
 
212
223
  const padding = 40; // 边界 padding 大小(像素)
213
224
  currentZoom = defaultZoom;
@@ -8,9 +8,8 @@ function zoomDiagram(direction) {
8
8
  const wrapper = container.querySelector('.diagram-viewer-svg-wrapper');
9
9
  if (!wrapper) return;
10
10
 
11
- // 获取原始尺寸
12
- const originalWidth = parseFloat(currentSvg.style.width) || parseFloat(currentSvg.getAttribute('width')) || 800;
13
- const originalHeight = parseFloat(currentSvg.style.height) || parseFloat(currentSvg.getAttribute('height')) || 600;
11
+ // 与查看器使用同一套尺寸解析逻辑,不能把 width="100%" 解析成 100。
12
+ const { width: originalWidth, height: originalHeight } = getDiagramIntrinsicDimensions(currentSvg);
14
13
 
15
14
  // 边界 padding 大小(与 diagram-viewer.js 中保持一致)
16
15
  const padding = 40;