docnest 0.1.13 → 0.1.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docnest",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "可复用的本地 Markdown 文档中心",
5
5
  "repository": {
6
6
  "type": "git",
@@ -179,13 +179,26 @@ body {
179
179
  }
180
180
 
181
181
  .doc-pdf-export-root {
182
+ /* PDF pages use a light paper palette, independently of the live reading mode. */
183
+ --linear-bg: #ffffff;
184
+ --linear-bg-subtle: #fafafa;
185
+ --linear-border: #e5e5e5;
186
+ --linear-text: #111111;
187
+ --linear-text-muted: #6b6b6b;
188
+ --linear-accent: #5e6ad2;
189
+ --linear-accent-hover: #4c57b8;
190
+ --doc-surface-raised: #ffffff;
191
+ --doc-rule-strong: #bfc0c5;
192
+ --doc-accent-soft: rgba(94, 106, 210, 0.12);
193
+ --doc-code-bg: #f6f7f8;
194
+ --doc-code-text: #24262b;
182
195
  position: absolute;
183
196
  left: 0;
184
197
  top: 0;
185
198
  width: 960px;
186
199
  padding: 24px 32px;
187
- background: #ffffff;
188
- color: #111111;
200
+ background: var(--doc-surface-raised);
201
+ color: var(--linear-text);
189
202
  z-index: -1;
190
203
  pointer-events: none;
191
204
  overflow: visible;
@@ -522,6 +535,7 @@ body[data-theme="dark"] .dir-toggle.active {
522
535
  /* 主内容区 - 白底、留白 */
523
536
  .main-content {
524
537
  flex: 1;
538
+ min-width: 0;
525
539
  overflow-y: auto;
526
540
  overflow-x: hidden;
527
541
  padding: 2rem 2.5rem;
@@ -535,6 +549,7 @@ body[data-theme="dark"] .dir-toggle.active {
535
549
  align-items: flex-start;
536
550
  gap: 24px;
537
551
  width: min(1400px, 100%);
552
+ min-width: 0;
538
553
  margin: 0 auto;
539
554
  }
540
555
 
@@ -1032,7 +1047,8 @@ body[data-theme="dark"] .dir-toggle.active {
1032
1047
  /* Markdown 内容 - 限宽、舒适行距;普通显示器上更宽,超宽屏用 92% 避免单行过长 */
1033
1048
  .markdown-body {
1034
1049
  flex: 1;
1035
- width: min(1024px, 100%);
1050
+ min-width: 0;
1051
+ width: min(var(--doc-reading-width, 1024px), 100%);
1036
1052
  max-width: 100%;
1037
1053
  margin: 0;
1038
1054
  box-sizing: border-box;
@@ -1050,7 +1066,7 @@ body[data-theme="dark"] .dir-toggle.active {
1050
1066
  }
1051
1067
 
1052
1068
  .doc-layout {
1053
- width: min(1024px, 100%);
1069
+ width: min(var(--doc-reading-width, 1024px), 100%);
1054
1070
  }
1055
1071
  }
1056
1072
 
@@ -1254,6 +1270,9 @@ body[data-theme="dark"] .not-found-path {
1254
1270
  text-align: center;
1255
1271
  background: var(--linear-bg-subtle);
1256
1272
  padding: 16px;
1273
+ min-width: 0;
1274
+ max-width: 100%;
1275
+ box-sizing: border-box;
1257
1276
  border-radius: var(--linear-radius);
1258
1277
  border: 1px solid var(--linear-border);
1259
1278
  overflow-x: auto;
@@ -1301,9 +1320,12 @@ body[data-theme="dark"] .markdown-body .mermaid::after {
1301
1320
 
1302
1321
  /* 确保 Mermaid SVG 正确显示 */
1303
1322
  .markdown-body .mermaid svg {
1304
- max-width: 100%;
1305
- height: auto;
1323
+ width: 100% !important;
1324
+ max-width: 100% !important;
1325
+ min-width: 0 !important;
1326
+ height: auto !important;
1306
1327
  display: block;
1328
+ margin: 0 auto;
1307
1329
  }
1308
1330
 
1309
1331
  /* Mermaid 下载按钮 */
@@ -2079,7 +2101,7 @@ body[data-theme="dark"] .tree-nav::-webkit-scrollbar-thumb:hover {
2079
2101
  .markdown-body .mermaid svg {
2080
2102
  max-width: 100% !important;
2081
2103
  max-height: 75vh !important; /* 高流程图缩放到单页内 */
2082
- width: auto !important;
2104
+ width: 100% !important;
2083
2105
  height: auto !important;
2084
2106
  display: block !important;
2085
2107
  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
 
@@ -377,7 +379,8 @@ body[data-doc-theme][data-theme="dark"] .pdf-export-modal__btn {
377
379
  }
378
380
 
379
381
  /* Theme 01: Slate Modern — the package's original visual language. */
380
- body[data-doc-theme="slate-modern"] {
382
+ body[data-doc-theme="slate-modern"],
383
+ .doc-pdf-export-root[data-doc-theme="slate-modern"] {
381
384
  --doc-font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
382
385
  "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
383
386
  --doc-font-reading: var(--doc-font-ui);
@@ -385,7 +388,8 @@ body[data-doc-theme="slate-modern"] {
385
388
  }
386
389
 
387
390
  /* Theme 02: Editorial Atlas */
388
- body[data-doc-theme="editorial-atlas"] {
391
+ body[data-doc-theme="editorial-atlas"],
392
+ .doc-pdf-export-root[data-doc-theme="editorial-atlas"] {
389
393
  --linear-bg: #f3f0e7;
390
394
  --linear-bg-subtle: #ebe6da;
391
395
  --linear-sidebar: #eee9dd;
@@ -405,7 +409,6 @@ body[data-doc-theme="editorial-atlas"] {
405
409
  "Microsoft YaHei", sans-serif;
406
410
  --doc-font-reading: "Songti SC", SimSun, "Noto Serif CJK SC", serif;
407
411
  --doc-font-display: "Songti SC", SimSun, "Noto Serif CJK SC", serif;
408
- --doc-reading-width: 860px;
409
412
  }
410
413
 
411
414
  body[data-doc-theme="editorial-atlas"][data-theme="dark"] {
@@ -452,18 +455,7 @@ body[data-doc-theme="editorial-atlas"] .sidebar {
452
455
  border-right-color: var(--doc-rule-strong);
453
456
  }
454
457
 
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
458
  body[data-doc-theme="editorial-atlas"] .markdown-body {
466
- width: min(var(--doc-reading-width), 100%);
467
459
  background: transparent;
468
460
  font-size: 1rem;
469
461
  line-height: 1.9;
@@ -500,7 +492,8 @@ body[data-doc-theme="editorial-atlas"] .doc-toc {
500
492
  }
501
493
 
502
494
  /* Theme 03: Precision Index */
503
- body[data-doc-theme="precision-index"] {
495
+ body[data-doc-theme="precision-index"],
496
+ .doc-pdf-export-root[data-doc-theme="precision-index"] {
504
497
  --linear-bg: #eef2f3;
505
498
  --linear-bg-subtle: #e4eaec;
506
499
  --linear-sidebar: #e9eef0;
@@ -520,7 +513,6 @@ body[data-doc-theme="precision-index"] {
520
513
  "Microsoft YaHei", sans-serif;
521
514
  --doc-font-reading: var(--doc-font-ui);
522
515
  --doc-font-display: var(--doc-font-ui);
523
- --doc-reading-width: 980px;
524
516
  }
525
517
 
526
518
  body[data-doc-theme="precision-index"][data-theme="dark"] {
@@ -597,28 +589,17 @@ body[data-doc-theme="precision-index"] .file-link.active {
597
589
  background: var(--doc-surface-raised);
598
590
  }
599
591
 
600
- body[data-doc-theme="precision-index"] .main-content {
601
- padding: 0;
602
- background: var(--linear-bg);
603
- }
604
-
605
592
  body[data-doc-theme="precision-index"] .breadcrumbs {
606
- margin: 0;
607
- padding: 0.75rem 1.5rem;
608
593
  border-bottom: 1px solid var(--doc-rule-strong);
609
594
  background: var(--linear-bg-subtle);
610
595
  font-size: 0.6875rem;
611
596
  }
612
597
 
613
598
  body[data-doc-theme="precision-index"] .doc-layout {
614
- width: 100%;
615
- gap: 0;
616
599
  background: var(--doc-surface-raised);
617
600
  }
618
601
 
619
602
  body[data-doc-theme="precision-index"] .markdown-body {
620
- width: min(var(--doc-reading-width), 100%);
621
- padding: clamp(2.5rem, 5vw, 5rem);
622
603
  border-right: 1px solid var(--doc-rule-strong);
623
604
  font-size: 0.9rem;
624
605
  line-height: 1.75;
@@ -651,11 +632,6 @@ body[data-doc-theme="precision-index"] .markdown-body h2 {
651
632
  }
652
633
 
653
634
  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
635
  border: 0;
660
636
  border-radius: 0;
661
637
  background: var(--linear-bg-subtle);
@@ -666,14 +642,9 @@ body[data-doc-theme="precision-index"] .doc-toc-title {
666
642
  text-transform: uppercase;
667
643
  }
668
644
 
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
645
  /* Theme 04: Archive Room */
676
- body[data-doc-theme="archive-room"] {
646
+ body[data-doc-theme="archive-room"],
647
+ .doc-pdf-export-root[data-doc-theme="archive-room"] {
677
648
  --linear-bg: #f3efe4;
678
649
  --linear-bg-subtle: #e9e2d3;
679
650
  --linear-sidebar: #ebe5d8;
@@ -693,7 +664,6 @@ body[data-doc-theme="archive-room"] {
693
664
  "Microsoft YaHei", sans-serif;
694
665
  --doc-font-reading: "Songti SC", SimSun, "Noto Serif CJK SC", serif;
695
666
  --doc-font-display: "Songti SC", SimSun, "Noto Serif CJK SC", serif;
696
- --doc-reading-width: 900px;
697
667
  }
698
668
 
699
669
  body[data-doc-theme="archive-room"][data-theme="dark"] {
@@ -761,7 +731,6 @@ body[data-doc-theme="archive-room"][data-theme="dark"] .file-link.active {
761
731
  }
762
732
 
763
733
  body[data-doc-theme="archive-room"] .main-content {
764
- padding: clamp(2rem, 4vw, 4.5rem);
765
734
  background-color: var(--linear-bg);
766
735
  background-image:
767
736
  linear-gradient(rgba(93, 96, 85, 0.045) 1px, transparent 1px),
@@ -769,14 +738,8 @@ body[data-doc-theme="archive-room"] .main-content {
769
738
  background-size: 24px 24px;
770
739
  }
771
740
 
772
- body[data-doc-theme="archive-room"] .doc-layout {
773
- width: min(1280px, 100%);
774
- gap: 34px;
775
- }
776
-
777
741
  body[data-doc-theme="archive-room"] .markdown-body {
778
- width: min(var(--doc-reading-width), 100%);
779
- padding: clamp(1.5rem, 3vw, 3rem);
742
+ padding: 1.5rem 1.5rem 3rem;
780
743
  border: 1px solid var(--doc-rule-strong);
781
744
  background: var(--doc-surface-raised);
782
745
  box-shadow: 7px 7px 0 var(--linear-border);
@@ -816,7 +779,8 @@ body[data-doc-theme="archive-room"] .doc-toc {
816
779
  }
817
780
 
818
781
  /* Theme 05: Swiss Manual */
819
- body[data-doc-theme="swiss-manual"] {
782
+ body[data-doc-theme="swiss-manual"],
783
+ .doc-pdf-export-root[data-doc-theme="swiss-manual"] {
820
784
  --linear-bg: #f4f3ef;
821
785
  --linear-bg-subtle: #e9e7e0;
822
786
  --linear-sidebar: #eeece6;
@@ -836,7 +800,6 @@ body[data-doc-theme="swiss-manual"] {
836
800
  "Microsoft YaHei", sans-serif;
837
801
  --doc-font-reading: var(--doc-font-ui);
838
802
  --doc-font-display: var(--doc-font-ui);
839
- --doc-reading-width: 960px;
840
803
  }
841
804
 
842
805
  body[data-doc-theme="swiss-manual"][data-theme="dark"] {
@@ -939,27 +902,13 @@ body[data-doc-theme="swiss-manual"] .file-link.active {
939
902
  font-weight: 800;
940
903
  }
941
904
 
942
- body[data-doc-theme="swiss-manual"] .main-content {
943
- padding: 0;
944
- background: var(--doc-surface-raised);
945
- }
946
-
947
905
  body[data-doc-theme="swiss-manual"] .breadcrumbs {
948
- margin: 0;
949
- padding: 0.85rem 1.5rem;
950
906
  border-bottom: 1px solid var(--doc-rule-strong);
951
907
  background: var(--linear-bg-subtle);
952
908
  font-size: 0.65rem;
953
909
  }
954
910
 
955
- body[data-doc-theme="swiss-manual"] .doc-layout {
956
- width: 100%;
957
- gap: 0;
958
- }
959
-
960
911
  body[data-doc-theme="swiss-manual"] .markdown-body {
961
- width: min(var(--doc-reading-width), 100%);
962
- padding: clamp(2.5rem, 6vw, 6rem);
963
912
  border-right: 1px solid var(--doc-rule-strong);
964
913
  font-size: 0.95rem;
965
914
  line-height: 1.72;
@@ -1000,22 +949,11 @@ body[data-doc-theme="swiss-manual"] .markdown-body blockquote {
1000
949
  }
1001
950
 
1002
951
  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
952
  border: 0;
1009
953
  border-radius: 0;
1010
954
  background: var(--linear-bg-subtle);
1011
955
  }
1012
956
 
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
957
  /* Shared component refinements for the four independent profiles. */
1020
958
  body:not([data-doc-theme="slate-modern"]) .markdown-body code:not(pre code) {
1021
959
  border: 1px solid var(--linear-border);
@@ -1206,26 +1144,11 @@ body[data-doc-theme="swiss-manual"].not-found-page .deep-space-404__code {
1206
1144
  overflow-y: auto;
1207
1145
  }
1208
1146
 
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
1147
  body[data-doc-theme="archive-room"] .markdown-body {
1215
1148
  padding: 1.25rem;
1216
1149
  box-shadow: 4px 4px 0 var(--linear-border);
1217
1150
  }
1218
1151
 
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
1152
  body[data-doc-theme="precision-index"] .markdown-body h1,
1230
1153
  body[data-doc-theme="swiss-manual"] .markdown-body h1 {
1231
1154
  font-size: clamp(2rem, 12vw, 3.25rem);
@@ -93,9 +93,40 @@
93
93
  function createExportRoot(article) {
94
94
  var root = document.createElement('div');
95
95
  root.className = EXPORT_CLASS;
96
+ root.setAttribute('data-doc-theme', document.body.getAttribute('data-doc-theme') || 'slate-modern');
97
+ root.setAttribute('data-theme', 'light');
96
98
 
97
99
  var clone = article.cloneNode(true);
98
100
  removeTransientNodes(clone);
101
+ // html2canvas may paint closed <details> descendants without reserving their
102
+ // height. Export static, expanded sections while leaving the live page alone.
103
+ Array.prototype.slice.call(clone.querySelectorAll('details')).reverse().forEach(function(details) {
104
+ var section = document.createElement('section');
105
+ Array.prototype.slice.call(details.attributes).forEach(function(attribute) {
106
+ if (attribute.name !== 'open') section.setAttribute(attribute.name, attribute.value);
107
+ });
108
+ section.style.display = 'block';
109
+ section.style.height = 'auto';
110
+ section.style.maxHeight = 'none';
111
+ section.style.overflow = 'visible';
112
+ section.style.margin = '1em 0';
113
+ while (details.firstChild) {
114
+ var child = details.firstChild;
115
+ if (child.tagName === 'SUMMARY') {
116
+ var heading = document.createElement('div');
117
+ heading.className = 'doc-pdf-section-title';
118
+ heading.style.fontWeight = '700';
119
+ heading.style.marginBottom = '12px';
120
+ while (child.firstChild) heading.appendChild(child.firstChild);
121
+ section.appendChild(heading);
122
+ details.removeChild(child);
123
+ } else {
124
+ section.appendChild(child);
125
+ }
126
+ }
127
+ details.parentNode.replaceChild(section, details);
128
+ });
129
+ clone.querySelectorAll('img').forEach(function(img) { img.loading = 'eager'; });
99
130
  root.appendChild(clone);
100
131
  document.body.appendChild(root);
101
132
 
@@ -137,18 +168,60 @@
137
168
  return breakpoints;
138
169
  }
139
170
 
171
+ function collectProtectedRegions(root, canvasScale) {
172
+ var rootRect = root.getBoundingClientRect();
173
+ var regions = [];
174
+ root.querySelectorAll('img, svg, canvas, figure, .mermaid').forEach(function(node) {
175
+ var rect = node.getBoundingClientRect();
176
+ if (rect.width <= 0 || rect.height <= 0) return;
177
+ regions.push({
178
+ top: Math.max(0, Math.floor((rect.top - rootRect.top) * canvasScale)),
179
+ bottom: Math.max(0, Math.ceil((rect.bottom - rootRect.top) * canvasScale)),
180
+ });
181
+ });
182
+ regions.sort(function(a, b) { return a.top - b.top || b.bottom - a.bottom; });
183
+ var merged = [];
184
+ regions.forEach(function(region) {
185
+ var last = merged[merged.length - 1];
186
+ if (last && region.top < last.bottom) {
187
+ last.bottom = Math.max(last.bottom, region.bottom);
188
+ } else {
189
+ merged.push(region);
190
+ }
191
+ });
192
+ return merged;
193
+ }
194
+
140
195
  function cleanupExportRoot(root) {
141
196
  if (root && root.parentNode) {
142
197
  root.parentNode.removeChild(root);
143
198
  }
144
199
  }
145
200
 
146
- function waitForExportReady() {
201
+ function waitForExportReady(root) {
147
202
  var fontReady = document.fonts && document.fonts.ready
148
203
  ? document.fonts.ready.catch(function() {})
149
204
  : Promise.resolve();
150
205
 
151
- return fontReady.then(function() {
206
+ var imagesReady = Promise.all(Array.prototype.map.call(root.querySelectorAll('img'), function(img) {
207
+ if (img.complete) {
208
+ return img.naturalWidth > 0 ? Promise.resolve() : Promise.reject(new Error('文档图片加载失败'));
209
+ }
210
+ return new Promise(function(resolve, reject) {
211
+ var timer = setTimeout(function() { finish(new Error('等待文档图片超时')); }, 15000);
212
+ function finish(error) {
213
+ clearTimeout(timer);
214
+ img.removeEventListener('load', loaded);
215
+ img.removeEventListener('error', failed);
216
+ if (error) reject(error); else resolve();
217
+ }
218
+ function loaded() { finish(); }
219
+ function failed() { finish(new Error('文档图片加载失败')); }
220
+ img.addEventListener('load', loaded, { once: true });
221
+ img.addEventListener('error', failed, { once: true });
222
+ });
223
+ }));
224
+ return Promise.all([fontReady, imagesReady]).then(function() {
152
225
  return new Promise(function(resolve) {
153
226
  requestAnimationFrame(function() {
154
227
  requestAnimationFrame(resolve);
@@ -169,8 +242,9 @@
169
242
  logging: false,
170
243
  width: width,
171
244
  height: height,
172
- windowWidth: width,
173
- windowHeight: height,
245
+ // Keep vw/vh styles and media queries identical to the measured DOM.
246
+ windowWidth: window.innerWidth,
247
+ windowHeight: window.innerHeight,
174
248
  scrollX: 0,
175
249
  scrollY: 0,
176
250
  });
@@ -313,7 +387,7 @@
313
387
  return Math.max(minBreakY - startY, searchStartY + bestRow - startY);
314
388
  }
315
389
 
316
- function addCanvasToPdf(pdf, canvas, margin, blockBreakpoints, watermark) {
390
+ function addCanvasToPdf(pdf, canvas, margin, blockBreakpoints, watermark, protectedRegions) {
317
391
  var pageWidth = pdf.internal.pageSize.getWidth();
318
392
  var pageHeight = pdf.internal.pageSize.getHeight();
319
393
  var usableWidth = pageWidth - margin * 2;
@@ -338,6 +412,16 @@
338
412
  sliceHeight = findBestPageBreak(canvas, sourceCtx, offsetY, pageCanvasHeight);
339
413
  }
340
414
  }
415
+ sliceHeight = Math.min(sliceHeight, remainingHeight, pageCanvasHeight);
416
+ var sliceEnd = offsetY + sliceHeight;
417
+ (protectedRegions || []).forEach(function(region) {
418
+ if (region.top < sliceEnd && region.bottom > sliceEnd) {
419
+ // Move a fitting image to the next page even if this leaves whitespace.
420
+ // A taller-than-page image receives one whole, scaled PDF page.
421
+ sliceEnd = region.top > offsetY ? region.top : Math.min(region.bottom, canvas.height);
422
+ }
423
+ });
424
+ sliceHeight = Math.max(1, sliceEnd - offsetY);
341
425
  var pageCanvas = document.createElement('canvas');
342
426
  pageCanvas.width = canvas.width;
343
427
  pageCanvas.height = sliceHeight;
@@ -362,7 +446,7 @@
362
446
  context,
363
447
  pageCanvas.width,
364
448
  pageCanvas.height,
365
- pageCanvasHeight,
449
+ Math.max(pageCanvasHeight, sliceHeight),
366
450
  watermark.text,
367
451
  );
368
452
  }
@@ -371,13 +455,14 @@
371
455
  pdf.addPage();
372
456
  }
373
457
 
374
- var imageHeight = (sliceHeight * usableWidth) / canvas.width;
458
+ var imageWidth = Math.min(usableWidth, (usableHeight * canvas.width) / sliceHeight);
459
+ var imageHeight = (sliceHeight * imageWidth) / canvas.width;
375
460
  pdf.addImage(
376
461
  pageCanvas.toDataURL('image/png'),
377
462
  'PNG',
463
+ margin + (usableWidth - imageWidth) / 2,
378
464
  margin,
379
- margin,
380
- usableWidth,
465
+ imageWidth,
381
466
  imageHeight,
382
467
  undefined,
383
468
  'FAST'
@@ -586,13 +671,15 @@
586
671
  format: 'a4',
587
672
  compress: true,
588
673
  });
589
- return waitForExportReady()
674
+ return waitForExportReady(exportRoot)
590
675
  .then(function() {
591
676
  return renderCanvas(exportRoot);
592
677
  })
593
678
  .then(function(canvas) {
594
- var blockBreakpoints = collectBlockBreakpoints(exportRoot, canvas.height / Math.max(exportRoot.scrollHeight, 1));
595
- addCanvasToPdf(pdf, canvas, margin, blockBreakpoints, watermark);
679
+ var canvasScale = canvas.height / Math.max(exportRoot.scrollHeight, 1);
680
+ var blockBreakpoints = collectBlockBreakpoints(exportRoot, canvasScale);
681
+ var protectedRegions = collectProtectedRegions(exportRoot, canvasScale);
682
+ addCanvasToPdf(pdf, canvas, margin, blockBreakpoints, watermark, protectedRegions);
596
683
  pdf.save(getExportFileName() + '.pdf');
597
684
  })
598
685
  .finally(function() {
@@ -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;