docnest 0.1.0
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/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +83 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +79 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +814 -0
- package/package.json +60 -0
- package/server/favicon.ico +0 -0
- package/server/static/css/highlight.css +72 -0
- package/server/static/css/styles.css +5762 -0
- package/server/static/favicon.ico +0 -0
- package/server/static/img/404-deep-space-light.webp +0 -0
- package/server/static/img/404-deep-space.webp +0 -0
- package/server/static/js/core/doc-client-nav.js +260 -0
- package/server/static/js/core/doc-pdf-export.js +579 -0
- package/server/static/js/core/mermaid-init.js +36 -0
- package/server/static/js/core/sidebar.js +141 -0
- package/server/static/js/core/theme.js +138 -0
- package/server/static/js/diagram/diagram-controls.js +90 -0
- package/server/static/js/diagram/diagram-download.js +594 -0
- package/server/static/js/diagram/diagram-drag.js +157 -0
- package/server/static/js/diagram/diagram-viewer.js +405 -0
- package/server/static/js/diagram/diagram-zoom.js +67 -0
- package/server/static/js/utils/app.js +14 -0
- package/server/static/js/utils/modal-stack.js +33 -0
- package/server/views/dir.ejs +142 -0
- package/server/views/doc.ejs +483 -0
- package/server/views/index.ejs +77 -0
- package/server/views/not-found.ejs +57 -0
- package/server/views/partials/breadcrumbs.ejs +15 -0
- package/server/views/partials/diagram-viewer.ejs +16 -0
- package/server/views/partials/header.ejs +37 -0
- package/server/views/partials/sidebar.ejs +19 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
var EXPORT_CLASS = 'doc-pdf-export-root';
|
|
3
|
+
|
|
4
|
+
function getAssetUrl(path) {
|
|
5
|
+
var base = document.querySelector('base');
|
|
6
|
+
if (base && base.getAttribute('href')) {
|
|
7
|
+
return base.getAttribute('href').replace(/\/?$/, '/') + path.replace(/^\//, '');
|
|
8
|
+
}
|
|
9
|
+
return path;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function loadScript(src, isReady) {
|
|
13
|
+
if (typeof isReady === 'function' && isReady()) {
|
|
14
|
+
return Promise.resolve();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return new Promise(function(resolve, reject) {
|
|
18
|
+
var existing = document.querySelector('script[data-dynamic-src="' + src + '"]');
|
|
19
|
+
if (existing) {
|
|
20
|
+
existing.addEventListener('load', function() {
|
|
21
|
+
resolve();
|
|
22
|
+
}, { once: true });
|
|
23
|
+
existing.addEventListener('error', function() {
|
|
24
|
+
reject(new Error('加载脚本失败: ' + src));
|
|
25
|
+
}, { once: true });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var script = document.createElement('script');
|
|
30
|
+
script.src = src;
|
|
31
|
+
script.async = true;
|
|
32
|
+
script.setAttribute('data-dynamic-src', src);
|
|
33
|
+
script.onload = function() {
|
|
34
|
+
resolve();
|
|
35
|
+
};
|
|
36
|
+
script.onerror = function() {
|
|
37
|
+
reject(new Error('加载脚本失败: ' + src));
|
|
38
|
+
};
|
|
39
|
+
document.head.appendChild(script);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ensurePdfLibs() {
|
|
44
|
+
return loadScript(
|
|
45
|
+
getAssetUrl('/vendor/html2canvas/html2canvas.min.js'),
|
|
46
|
+
function() { return typeof window.html2canvas !== 'undefined'; }
|
|
47
|
+
).then(function() {
|
|
48
|
+
return loadScript(
|
|
49
|
+
getAssetUrl('/vendor/dompurify/purify.min.js'),
|
|
50
|
+
function() { return typeof window.DOMPurify !== 'undefined'; }
|
|
51
|
+
);
|
|
52
|
+
}).then(function() {
|
|
53
|
+
return loadScript(
|
|
54
|
+
getAssetUrl('/vendor/jspdf/jspdf.umd.min.js'),
|
|
55
|
+
function() {
|
|
56
|
+
return typeof window.jspdf !== 'undefined' || typeof window.jsPDF !== 'undefined';
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getJsPdfCtor() {
|
|
63
|
+
if (typeof window.jspdf !== 'undefined') {
|
|
64
|
+
return window.jspdf.jsPDF || window.jspdf;
|
|
65
|
+
}
|
|
66
|
+
if (typeof window.jsPDF !== 'undefined') {
|
|
67
|
+
return window.jsPDF;
|
|
68
|
+
}
|
|
69
|
+
throw new Error('jsPDF 未正确加载');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sanitizeFileName(name) {
|
|
73
|
+
return (name || 'document')
|
|
74
|
+
.replace(/[\\/:*?"<>|]+/g, '-')
|
|
75
|
+
.replace(/\s+/g, ' ')
|
|
76
|
+
.trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getExportFileName() {
|
|
80
|
+
var currentPath = window.currentDocPath || document.body.getAttribute('data-current-path') || '';
|
|
81
|
+
var name = currentPath.split('/').pop() || document.title || 'document';
|
|
82
|
+
name = name.replace(/\.(md|html)$/i, '');
|
|
83
|
+
name = name.replace(/\s*-\s*文档中心\s*$/i, '');
|
|
84
|
+
return sanitizeFileName(name);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function removeTransientNodes(root) {
|
|
88
|
+
root.querySelectorAll('.mermaid-download-btn').forEach(function(node) {
|
|
89
|
+
node.remove();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createExportRoot(article) {
|
|
94
|
+
var root = document.createElement('div');
|
|
95
|
+
root.className = EXPORT_CLASS;
|
|
96
|
+
|
|
97
|
+
var clone = article.cloneNode(true);
|
|
98
|
+
removeTransientNodes(clone);
|
|
99
|
+
root.appendChild(clone);
|
|
100
|
+
document.body.appendChild(root);
|
|
101
|
+
|
|
102
|
+
return root;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function collectBlockBreakpoints(root, canvasScale) {
|
|
106
|
+
var rootRect = root.getBoundingClientRect();
|
|
107
|
+
var selector = [
|
|
108
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
109
|
+
'p', 'li', 'blockquote', 'pre', 'table',
|
|
110
|
+
'hr', '.mermaid'
|
|
111
|
+
].join(',');
|
|
112
|
+
var nodes = Array.prototype.slice.call(root.querySelectorAll(selector));
|
|
113
|
+
var breakpoints = [];
|
|
114
|
+
var seen = new Set();
|
|
115
|
+
|
|
116
|
+
nodes.forEach(function(node) {
|
|
117
|
+
var rect = node.getBoundingClientRect();
|
|
118
|
+
if (!rect || rect.height <= 0) return;
|
|
119
|
+
|
|
120
|
+
var top = Math.max(0, rect.top - rootRect.top);
|
|
121
|
+
var bottom = Math.max(top, rect.bottom - rootRect.top);
|
|
122
|
+
var key = Math.round(bottom);
|
|
123
|
+
|
|
124
|
+
if (seen.has(key)) return;
|
|
125
|
+
seen.add(key);
|
|
126
|
+
|
|
127
|
+
breakpoints.push({
|
|
128
|
+
top: Math.round(top * canvasScale),
|
|
129
|
+
bottom: Math.round(bottom * canvasScale),
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
breakpoints.sort(function(a, b) {
|
|
134
|
+
return a.bottom - b.bottom;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return breakpoints;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function cleanupExportRoot(root) {
|
|
141
|
+
if (root && root.parentNode) {
|
|
142
|
+
root.parentNode.removeChild(root);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function waitForExportReady() {
|
|
147
|
+
var fontReady = document.fonts && document.fonts.ready
|
|
148
|
+
? document.fonts.ready.catch(function() {})
|
|
149
|
+
: Promise.resolve();
|
|
150
|
+
|
|
151
|
+
return fontReady.then(function() {
|
|
152
|
+
return new Promise(function(resolve) {
|
|
153
|
+
requestAnimationFrame(function() {
|
|
154
|
+
requestAnimationFrame(resolve);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function renderCanvas(root) {
|
|
161
|
+
var width = Math.max(root.scrollWidth, root.offsetWidth, 960);
|
|
162
|
+
var height = Math.max(root.scrollHeight, root.offsetHeight, 1);
|
|
163
|
+
|
|
164
|
+
return window.html2canvas(root, {
|
|
165
|
+
scale: 2,
|
|
166
|
+
useCORS: true,
|
|
167
|
+
allowTaint: true,
|
|
168
|
+
backgroundColor: '#ffffff',
|
|
169
|
+
logging: false,
|
|
170
|
+
width: width,
|
|
171
|
+
height: height,
|
|
172
|
+
windowWidth: width,
|
|
173
|
+
windowHeight: height,
|
|
174
|
+
scrollX: 0,
|
|
175
|
+
scrollY: 0,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getCanvasContext(canvas) {
|
|
180
|
+
return canvas.getContext('2d', { willReadFrequently: true }) || canvas.getContext('2d');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getRowDensityScores(sourceCtx, width, startY, endY) {
|
|
184
|
+
var bandHeight = Math.max(1, endY - startY);
|
|
185
|
+
var imageData = sourceCtx.getImageData(0, startY, width, bandHeight).data;
|
|
186
|
+
var rowStride = width * 4;
|
|
187
|
+
var scores = new Array(bandHeight);
|
|
188
|
+
|
|
189
|
+
for (var row = 0; row < bandHeight; row++) {
|
|
190
|
+
var offset = row * rowStride;
|
|
191
|
+
var darkPixelCount = 0;
|
|
192
|
+
|
|
193
|
+
for (var x = 0; x < rowStride; x += 4) {
|
|
194
|
+
var r = imageData[offset + x];
|
|
195
|
+
var g = imageData[offset + x + 1];
|
|
196
|
+
var b = imageData[offset + x + 2];
|
|
197
|
+
|
|
198
|
+
if (r + g + b < 735) {
|
|
199
|
+
darkPixelCount += 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
scores[row] = darkPixelCount / width;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return scores;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function findBreakByBlocks(blockBreakpoints, startY, preferredHeight) {
|
|
210
|
+
if (!blockBreakpoints || blockBreakpoints.length === 0) return null;
|
|
211
|
+
|
|
212
|
+
var idealBreakY = startY + preferredHeight;
|
|
213
|
+
var minBreakY = startY + Math.max(160, Math.floor(preferredHeight * 0.7));
|
|
214
|
+
var candidate = null;
|
|
215
|
+
|
|
216
|
+
for (var i = 0; i < blockBreakpoints.length; i++) {
|
|
217
|
+
var breakpoint = blockBreakpoints[i];
|
|
218
|
+
var candidates = [breakpoint.top, breakpoint.bottom];
|
|
219
|
+
|
|
220
|
+
for (var j = 0; j < candidates.length; j++) {
|
|
221
|
+
var edge = candidates[j];
|
|
222
|
+
if (edge <= minBreakY) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (edge > idealBreakY) {
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
candidate = edge;
|
|
229
|
+
}
|
|
230
|
+
if (breakpoint.top > idealBreakY || breakpoint.bottom > idealBreakY) {
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return candidate ? candidate - startY : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function findBestPageBreak(canvas, sourceCtx, startY, preferredHeight) {
|
|
239
|
+
var idealBreakY = Math.min(startY + preferredHeight, canvas.height);
|
|
240
|
+
var minBreakY = Math.max(startY + Math.floor(preferredHeight * 0.72), startY + 80);
|
|
241
|
+
var searchRadius = Math.min(180, Math.max(72, Math.floor(preferredHeight * 0.12)));
|
|
242
|
+
var searchStartY = Math.max(minBreakY, idealBreakY - searchRadius);
|
|
243
|
+
var searchEndY = Math.min(canvas.height, idealBreakY + searchRadius);
|
|
244
|
+
|
|
245
|
+
if (searchEndY <= searchStartY + 4) {
|
|
246
|
+
return idealBreakY - startY;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
var rowScores = getRowDensityScores(sourceCtx, canvas.width, searchStartY, searchEndY);
|
|
250
|
+
var lowDensityThreshold = 0.0015;
|
|
251
|
+
var minBandHeight = 10;
|
|
252
|
+
var bestBand = null;
|
|
253
|
+
var bandStart = -1;
|
|
254
|
+
|
|
255
|
+
for (var row = 0; row <= rowScores.length; row++) {
|
|
256
|
+
var isLowDensity = row < rowScores.length && rowScores[row] <= lowDensityThreshold;
|
|
257
|
+
|
|
258
|
+
if (isLowDensity && bandStart === -1) {
|
|
259
|
+
bandStart = row;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if ((!isLowDensity || row === rowScores.length) && bandStart !== -1) {
|
|
263
|
+
var bandEnd = row - 1;
|
|
264
|
+
var bandHeight = bandEnd - bandStart + 1;
|
|
265
|
+
|
|
266
|
+
if (bandHeight >= minBandHeight) {
|
|
267
|
+
var bandCenter = searchStartY + bandStart + Math.floor(bandHeight / 2);
|
|
268
|
+
var bandDistance = Math.abs(bandCenter - idealBreakY);
|
|
269
|
+
|
|
270
|
+
if (!bestBand || bandDistance < bestBand.distance || (bandDistance === bestBand.distance && bandHeight > bestBand.height)) {
|
|
271
|
+
bestBand = {
|
|
272
|
+
center: bandCenter,
|
|
273
|
+
distance: bandDistance,
|
|
274
|
+
height: bandHeight,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
bandStart = -1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (bestBand) {
|
|
284
|
+
return Math.max(minBreakY - startY, bestBand.center - startY);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
var bestRow = idealBreakY - searchStartY;
|
|
288
|
+
var bestScore = Number.POSITIVE_INFINITY;
|
|
289
|
+
var bestDistance = Number.POSITIVE_INFINITY;
|
|
290
|
+
|
|
291
|
+
for (var rowIndex = 0; rowIndex < rowScores.length; rowIndex++) {
|
|
292
|
+
var windowStart = Math.max(0, rowIndex - 4);
|
|
293
|
+
var windowEnd = Math.min(rowScores.length - 1, rowIndex + 4);
|
|
294
|
+
var total = 0;
|
|
295
|
+
var count = 0;
|
|
296
|
+
|
|
297
|
+
for (var i = windowStart; i <= windowEnd; i++) {
|
|
298
|
+
total += rowScores[i];
|
|
299
|
+
count += 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
var score = total / count;
|
|
303
|
+
var candidateY = searchStartY + rowIndex;
|
|
304
|
+
var distance = Math.abs(candidateY - idealBreakY);
|
|
305
|
+
|
|
306
|
+
if (score < bestScore - 0.0001 || (Math.abs(score - bestScore) <= 0.0001 && distance < bestDistance)) {
|
|
307
|
+
bestScore = score;
|
|
308
|
+
bestDistance = distance;
|
|
309
|
+
bestRow = rowIndex;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return Math.max(minBreakY - startY, searchStartY + bestRow - startY);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function addCanvasToPdf(pdf, canvas, margin, blockBreakpoints, watermarkEnabled, watermarkText) {
|
|
317
|
+
var pageWidth = pdf.internal.pageSize.getWidth();
|
|
318
|
+
var pageHeight = pdf.internal.pageSize.getHeight();
|
|
319
|
+
var usableWidth = pageWidth - margin * 2;
|
|
320
|
+
var usableHeight = pageHeight - margin * 2;
|
|
321
|
+
var pageCanvasHeight = Math.max(
|
|
322
|
+
1,
|
|
323
|
+
Math.floor((usableHeight * canvas.width) / usableWidth)
|
|
324
|
+
);
|
|
325
|
+
var sourceCtx = getCanvasContext(canvas);
|
|
326
|
+
var offsetY = 0;
|
|
327
|
+
var pageIndex = 0;
|
|
328
|
+
|
|
329
|
+
while (offsetY < canvas.height) {
|
|
330
|
+
var remainingHeight = canvas.height - offsetY;
|
|
331
|
+
var sliceHeight;
|
|
332
|
+
|
|
333
|
+
if (remainingHeight <= pageCanvasHeight) {
|
|
334
|
+
sliceHeight = remainingHeight;
|
|
335
|
+
} else {
|
|
336
|
+
sliceHeight = findBreakByBlocks(blockBreakpoints, offsetY, pageCanvasHeight);
|
|
337
|
+
if (!sliceHeight) {
|
|
338
|
+
sliceHeight = findBestPageBreak(canvas, sourceCtx, offsetY, pageCanvasHeight);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
var pageCanvas = document.createElement('canvas');
|
|
342
|
+
pageCanvas.width = canvas.width;
|
|
343
|
+
pageCanvas.height = sliceHeight;
|
|
344
|
+
|
|
345
|
+
var context = getCanvasContext(pageCanvas);
|
|
346
|
+
context.fillStyle = '#ffffff';
|
|
347
|
+
context.fillRect(0, 0, pageCanvas.width, pageCanvas.height);
|
|
348
|
+
context.drawImage(
|
|
349
|
+
canvas,
|
|
350
|
+
0,
|
|
351
|
+
offsetY,
|
|
352
|
+
canvas.width,
|
|
353
|
+
sliceHeight,
|
|
354
|
+
0,
|
|
355
|
+
0,
|
|
356
|
+
canvas.width,
|
|
357
|
+
sliceHeight
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
if (watermarkEnabled) {
|
|
361
|
+
drawWatermarkPattern(context, pageCanvas.width, pageCanvas.height, pageCanvasHeight, watermarkText);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (pageIndex > 0) {
|
|
365
|
+
pdf.addPage();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
var imageHeight = (sliceHeight * usableWidth) / canvas.width;
|
|
369
|
+
pdf.addImage(
|
|
370
|
+
pageCanvas.toDataURL('image/png'),
|
|
371
|
+
'PNG',
|
|
372
|
+
margin,
|
|
373
|
+
margin,
|
|
374
|
+
usableWidth,
|
|
375
|
+
imageHeight,
|
|
376
|
+
undefined,
|
|
377
|
+
'FAST'
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
offsetY += sliceHeight;
|
|
381
|
+
pageIndex += 1;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function drawWatermarkPattern(context, width, height, referenceHeight, watermarkText) {
|
|
386
|
+
if (!watermarkText) return;
|
|
387
|
+
var tileWidth = width / 4;
|
|
388
|
+
var tileHeight = referenceHeight / 2;
|
|
389
|
+
var fontSize = Math.round(Math.min(tileWidth, tileHeight) * 0.38);
|
|
390
|
+
var startX = tileWidth / 2;
|
|
391
|
+
var startY = tileHeight / 2;
|
|
392
|
+
|
|
393
|
+
context.save();
|
|
394
|
+
context.textAlign = 'center';
|
|
395
|
+
context.textBaseline = 'middle';
|
|
396
|
+
context.fillStyle = 'rgba(30, 41, 59, 0.055)';
|
|
397
|
+
context.font = '700 ' + fontSize + 'px "PingFang SC", "Microsoft YaHei", sans-serif';
|
|
398
|
+
|
|
399
|
+
for (var y = startY; y < height + tileHeight; y += tileHeight) {
|
|
400
|
+
var rowOffsetX = Math.round(((y - startY) / tileHeight) % 2) * (tileWidth / 2);
|
|
401
|
+
for (var x = startX; x < width + tileWidth; x += tileWidth) {
|
|
402
|
+
context.save();
|
|
403
|
+
context.translate(x + rowOffsetX, y);
|
|
404
|
+
context.rotate(-Math.PI / 5.4);
|
|
405
|
+
context.fillText(watermarkText, 0, 0);
|
|
406
|
+
context.restore();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
context.restore();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function openPdfExportDialog() {
|
|
414
|
+
return new Promise(function(resolve) {
|
|
415
|
+
var config = window.DOCNEST_CONFIG || {};
|
|
416
|
+
var defaultWatermarkEnabled = config.watermarkEnabled === true;
|
|
417
|
+
var defaultWatermarkText = String(config.watermarkText || document.title || '');
|
|
418
|
+
var modalId = 'pdf-export-modal';
|
|
419
|
+
var overlay = document.createElement('div');
|
|
420
|
+
overlay.className = 'pdf-export-modal';
|
|
421
|
+
overlay.innerHTML = [
|
|
422
|
+
'<div class="pdf-export-modal__panel" role="dialog" aria-modal="true" aria-labelledby="pdf-export-title">',
|
|
423
|
+
'<div class="pdf-export-modal__title" id="pdf-export-title">下载 PDF</div>',
|
|
424
|
+
'<div class="pdf-export-modal__desc">导出前确认本次 PDF 是否需要水印;本次修改不会写回项目配置。</div>',
|
|
425
|
+
'<label class="pdf-export-modal__option">',
|
|
426
|
+
'<input type="checkbox" id="pdf-export-watermark-checkbox">',
|
|
427
|
+
'<span class="pdf-export-modal__option-text">启用水印</span>',
|
|
428
|
+
'</label>',
|
|
429
|
+
'<label class="pdf-export-modal__watermark-field" for="pdf-export-watermark-text">水印文字</label>',
|
|
430
|
+
'<input class="pdf-export-modal__watermark-input" id="pdf-export-watermark-text" type="text" maxlength="80">',
|
|
431
|
+
'<div class="pdf-export-modal__actions">',
|
|
432
|
+
'<button type="button" class="pdf-export-modal__btn" data-action="cancel">取消</button>',
|
|
433
|
+
'<button type="button" class="pdf-export-modal__btn pdf-export-modal__btn--primary" data-action="confirm">确认下载</button>',
|
|
434
|
+
'</div>',
|
|
435
|
+
'</div>'
|
|
436
|
+
].join('');
|
|
437
|
+
|
|
438
|
+
function close(result) {
|
|
439
|
+
if (typeof removeModal === 'function') {
|
|
440
|
+
removeModal(modalId);
|
|
441
|
+
}
|
|
442
|
+
if (overlay.parentNode) {
|
|
443
|
+
overlay.parentNode.removeChild(overlay);
|
|
444
|
+
}
|
|
445
|
+
resolve(result);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
overlay.addEventListener('click', function(event) {
|
|
449
|
+
if (event.target === overlay) {
|
|
450
|
+
close(null);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
overlay.querySelector('[data-action="cancel"]').addEventListener('click', function() {
|
|
455
|
+
close(null);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
overlay.querySelector('[data-action="confirm"]').addEventListener('click', function() {
|
|
459
|
+
var checkbox = overlay.querySelector('#pdf-export-watermark-checkbox');
|
|
460
|
+
var watermarkInput = overlay.querySelector('#pdf-export-watermark-text');
|
|
461
|
+
close({
|
|
462
|
+
watermarkEnabled: !!(checkbox && checkbox.checked),
|
|
463
|
+
watermarkText: watermarkInput ? watermarkInput.value.trim() : defaultWatermarkText,
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
document.body.appendChild(overlay);
|
|
468
|
+
if (typeof pushModal === 'function') {
|
|
469
|
+
pushModal(modalId, function() {
|
|
470
|
+
close(null);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
var checkbox = overlay.querySelector('#pdf-export-watermark-checkbox');
|
|
475
|
+
if (checkbox) {
|
|
476
|
+
checkbox.checked = defaultWatermarkEnabled;
|
|
477
|
+
checkbox.addEventListener('change', function() {
|
|
478
|
+
var watermarkInput = overlay.querySelector('#pdf-export-watermark-text');
|
|
479
|
+
if (watermarkInput) watermarkInput.disabled = !checkbox.checked;
|
|
480
|
+
});
|
|
481
|
+
checkbox.focus();
|
|
482
|
+
}
|
|
483
|
+
var watermarkInput = overlay.querySelector('#pdf-export-watermark-text');
|
|
484
|
+
if (watermarkInput) {
|
|
485
|
+
watermarkInput.value = defaultWatermarkText;
|
|
486
|
+
watermarkInput.disabled = !defaultWatermarkEnabled;
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function setButtonLoading(button, loading) {
|
|
492
|
+
if (!button) return;
|
|
493
|
+
if (loading) {
|
|
494
|
+
button.disabled = true;
|
|
495
|
+
button.classList.add('is-loading');
|
|
496
|
+
button.dataset.originalText = button.querySelector('.header-action-text')
|
|
497
|
+
? button.querySelector('.header-action-text').textContent || ''
|
|
498
|
+
: '';
|
|
499
|
+
var loadingText = button.querySelector('.header-action-text');
|
|
500
|
+
if (loadingText) loadingText.textContent = '导出中...';
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
button.disabled = false;
|
|
505
|
+
button.classList.remove('is-loading');
|
|
506
|
+
var text = button.querySelector('.header-action-text');
|
|
507
|
+
if (text && button.dataset.originalText) {
|
|
508
|
+
text.textContent = button.dataset.originalText;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function exportCurrentDoc(button, options) {
|
|
513
|
+
var article = document.querySelector('.main-content .markdown-body');
|
|
514
|
+
if (!article) {
|
|
515
|
+
window.alert('未找到当前文档内容,无法导出 PDF。');
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
setButtonLoading(button, true);
|
|
520
|
+
|
|
521
|
+
ensurePdfLibs()
|
|
522
|
+
.then(function() {
|
|
523
|
+
var JsPdf = getJsPdfCtor();
|
|
524
|
+
var exportRoot = createExportRoot(article);
|
|
525
|
+
var margin = 12;
|
|
526
|
+
var pdf = new JsPdf({
|
|
527
|
+
orientation: 'portrait',
|
|
528
|
+
unit: 'mm',
|
|
529
|
+
format: 'a4',
|
|
530
|
+
compress: true,
|
|
531
|
+
});
|
|
532
|
+
var config = window.DOCNEST_CONFIG || {};
|
|
533
|
+
var watermarkEnabled = options && typeof options.watermarkEnabled === 'boolean'
|
|
534
|
+
? options.watermarkEnabled
|
|
535
|
+
: config.watermarkEnabled === true;
|
|
536
|
+
var watermarkText = options && options.watermarkText
|
|
537
|
+
? options.watermarkText
|
|
538
|
+
: String(config.watermarkText || document.title || '');
|
|
539
|
+
|
|
540
|
+
return waitForExportReady()
|
|
541
|
+
.then(function() {
|
|
542
|
+
return renderCanvas(exportRoot);
|
|
543
|
+
})
|
|
544
|
+
.then(function(canvas) {
|
|
545
|
+
var blockBreakpoints = collectBlockBreakpoints(exportRoot, canvas.height / Math.max(exportRoot.scrollHeight, 1));
|
|
546
|
+
addCanvasToPdf(pdf, canvas, margin, blockBreakpoints, watermarkEnabled, watermarkText);
|
|
547
|
+
pdf.save(getExportFileName() + '.pdf');
|
|
548
|
+
})
|
|
549
|
+
.finally(function() {
|
|
550
|
+
cleanupExportRoot(exportRoot);
|
|
551
|
+
});
|
|
552
|
+
})
|
|
553
|
+
.catch(function(error) {
|
|
554
|
+
console.error('导出 PDF 失败:', error);
|
|
555
|
+
window.alert('导出 PDF 失败,请稍后重试。');
|
|
556
|
+
})
|
|
557
|
+
.finally(function() {
|
|
558
|
+
setButtonLoading(button, false);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function init() {
|
|
563
|
+
var button = document.getElementById('download-doc-pdf-btn');
|
|
564
|
+
if (!button) return;
|
|
565
|
+
|
|
566
|
+
button.addEventListener('click', function() {
|
|
567
|
+
openPdfExportDialog().then(function(result) {
|
|
568
|
+
if (!result) return;
|
|
569
|
+
exportCurrentDoc(button, result);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (document.readyState === 'loading') {
|
|
575
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
576
|
+
} else {
|
|
577
|
+
init();
|
|
578
|
+
}
|
|
579
|
+
})();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Mermaid 初始化
|
|
2
|
+
(function() {
|
|
3
|
+
if (typeof mermaid === 'undefined') return;
|
|
4
|
+
|
|
5
|
+
// 初始化函数,在 DOM 加载完成后执行
|
|
6
|
+
function initMermaid() {
|
|
7
|
+
// 安全地获取主题,如果 body 不存在则使用默认值
|
|
8
|
+
const initialTheme = (document.body && document.body.getAttribute('data-theme')) || 'light';
|
|
9
|
+
|
|
10
|
+
mermaid.initialize({
|
|
11
|
+
startOnLoad: true,
|
|
12
|
+
theme: initialTheme === 'dark' ? 'dark' : 'default',
|
|
13
|
+
securityLevel: 'loose',
|
|
14
|
+
flowchart: {
|
|
15
|
+
useMaxWidth: true,
|
|
16
|
+
htmlLabels: true
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// 手动渲染所有 Mermaid 图表
|
|
21
|
+
mermaid.run().then(() => {
|
|
22
|
+
// 为所有 Mermaid 图表添加点击事件
|
|
23
|
+
if (typeof setupDiagramViewer === 'function') {
|
|
24
|
+
setupDiagramViewer();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 确保在 DOM 加载完成后执行
|
|
30
|
+
if (document.readyState === 'loading') {
|
|
31
|
+
document.addEventListener('DOMContentLoaded', initMermaid);
|
|
32
|
+
} else {
|
|
33
|
+
// DOM 已经加载完成,直接执行
|
|
34
|
+
initMermaid();
|
|
35
|
+
}
|
|
36
|
+
})();
|