docnest 0.1.11 → 0.1.12
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 +13 -2
- package/dist/cli.js +1 -2
- package/dist/config.d.ts +6 -9
- package/dist/config.js +1 -7
- package/dist/server.js +16 -10
- package/docnest.config.example.mjs +3 -6
- package/package.json +3 -2
- package/server/static/css/styles.css +49 -1
- package/server/static/css/themes.css +53 -2
- package/server/static/js/core/doc-pdf-export.js +27 -37
- package/server/static/js/diagram/diagram-download.js +8 -0
- package/server/static/js/diagram/diagram-viewer.js +6 -0
- package/server/views/dir.ejs +4 -1
- package/server/views/doc.ejs +27 -19
- package/server/views/index.ejs +11 -8
- package/server/views/not-found.ejs +1 -0
- package/server/views/partials/appearance-head.ejs +2 -2
- package/server/views/partials/header.ejs +2 -2
- package/server/views/partials/page-protection.ejs +31 -0
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ npx docnest serve
|
|
|
20
20
|
|
|
21
21
|
## 配置
|
|
22
22
|
|
|
23
|
-
复制 `docnest.config.example.mjs` 为项目根目录的 `docnest.config.mjs
|
|
23
|
+
复制 `docnest.config.example.mjs` 为项目根目录的 `docnest.config.mjs`,配置项目自己的标题、目录顺序、存储键前缀和阅读限制策略。
|
|
24
24
|
|
|
25
25
|
### 外观主题
|
|
26
26
|
|
|
@@ -56,7 +56,18 @@ export default defineConfig({
|
|
|
56
56
|
|
|
57
57
|
未配置 `appearance` 时使用 `slate-modern + auto`。旧配置中的 `current-docs` 会自动迁移到 `slate-modern`,正文视觉不会发生变化;旧版保存的 `markdown-theme` 明暗偏好也会自动迁移到新的项目级存储键。
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
PDF、页面水印和图表外带能力由一个开关统一控制,默认使用普通阅读模式:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
export default defineConfig({
|
|
63
|
+
restrictedMode: false,
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- `restrictedMode: false`:显示打印和 PDF 导出,允许 Mermaid 图下载与大图查看。导出 PDF 时会弹出本次水印设置,可选择是否添加并自定义文案;默认文案为 `site.title`,不会写回配置。
|
|
68
|
+
- `restrictedMode: true`:隐藏打印和 PDF 导出入口且不加载导出模块;使用 `site.title` 作为固定水印,禁止文字选择、复制、剪切和拖拽,并隐藏 Mermaid 下载与大图查看入口。Mermaid 仍会在正文中正常渲染。
|
|
69
|
+
|
|
70
|
+
受限模式只移除 DocNest 提供的页面入口和脚本,不承诺对浏览器开发者工具或系统级截图提供绝对防护。
|
|
60
71
|
|
|
61
72
|
### 极简单口令授权
|
|
62
73
|
|
package/dist/cli.js
CHANGED
|
@@ -62,8 +62,7 @@ function exposeConfig(config) {
|
|
|
62
62
|
process.env.DOCS_THEME_DEFAULT = config.appearance.defaultTheme;
|
|
63
63
|
process.env.DOCS_THEME_MODE_DEFAULT = config.appearance.defaultMode;
|
|
64
64
|
process.env.DOCS_THEME_ENABLED = JSON.stringify(config.appearance.enabledThemes);
|
|
65
|
-
process.env.
|
|
66
|
-
process.env.DOCS_WATERMARK_TEXT = config.export.watermark.text;
|
|
65
|
+
process.env.DOCS_RESTRICTED_MODE = String(config.restrictedMode);
|
|
67
66
|
process.env.DOCS_HOST = config.server.host;
|
|
68
67
|
process.env.DOCS_PORT_START = String(config.server.port);
|
|
69
68
|
process.env.DOCS_WATCH_ENABLED = String(config.server.watch);
|
package/dist/config.d.ts
CHANGED
|
@@ -32,12 +32,11 @@ export type DocNestConfig = {
|
|
|
32
32
|
localStorageKey?: string;
|
|
33
33
|
sessionTtlMinutes?: number;
|
|
34
34
|
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
};
|
|
35
|
+
/**
|
|
36
|
+
* Restrict the reading experience to in-browser viewing only.
|
|
37
|
+
* Restricted mode also enables the fixed site-title watermark.
|
|
38
|
+
*/
|
|
39
|
+
restrictedMode?: boolean;
|
|
41
40
|
};
|
|
42
41
|
export declare function defineConfig(config: DocNestConfig): DocNestConfig;
|
|
43
42
|
export type ResolvedDocNestConfig = Required<DocNestConfig> & {
|
|
@@ -54,9 +53,7 @@ export type ResolvedDocNestConfig = Required<DocNestConfig> & {
|
|
|
54
53
|
localStorageKey: string;
|
|
55
54
|
sessionTtlMinutes: number;
|
|
56
55
|
};
|
|
57
|
-
|
|
58
|
-
watermark: Required<NonNullable<NonNullable<DocNestConfig['export']>['watermark']>>;
|
|
59
|
-
};
|
|
56
|
+
restrictedMode: boolean;
|
|
60
57
|
};
|
|
61
58
|
export declare function loadConfig(projectRoot?: string): Promise<ResolvedDocNestConfig>;
|
|
62
59
|
export {};
|
package/dist/config.js
CHANGED
|
@@ -87,7 +87,6 @@ async function loadConfig(projectRoot = process.cwd()) {
|
|
|
87
87
|
const appearance = userConfig.appearance ?? {};
|
|
88
88
|
const server = userConfig.server ?? {};
|
|
89
89
|
const auth = userConfig.auth ?? {};
|
|
90
|
-
const watermark = userConfig.export?.watermark ?? {};
|
|
91
90
|
const storageKeyPrefix = site.storageKeyPrefix?.trim() || `docnest:${projectName}`;
|
|
92
91
|
const authLocalStorageKey = auth.localStorageKey?.trim() || `${storageKeyPrefix}:auth-passphrase`;
|
|
93
92
|
return {
|
|
@@ -118,11 +117,6 @@ async function loadConfig(projectRoot = process.cwd()) {
|
|
|
118
117
|
localStorageKey: authLocalStorageKey,
|
|
119
118
|
sessionTtlMinutes: normalizePositiveNumber(auth.sessionTtlMinutes, 24 * 60),
|
|
120
119
|
},
|
|
121
|
-
|
|
122
|
-
watermark: {
|
|
123
|
-
enabled: watermark.enabled ?? false,
|
|
124
|
-
text: watermark.text?.trim() || site.title?.trim() || projectName,
|
|
125
|
-
},
|
|
126
|
-
},
|
|
120
|
+
restrictedMode: userConfig.restrictedMode === true,
|
|
127
121
|
};
|
|
128
122
|
}
|
package/dist/server.js
CHANGED
|
@@ -54,8 +54,9 @@ const DOCS_THEME_ENABLED = (() => {
|
|
|
54
54
|
return [...DOCS_THEME_NAMES];
|
|
55
55
|
}
|
|
56
56
|
})();
|
|
57
|
-
const
|
|
58
|
-
const
|
|
57
|
+
const DOCS_RESTRICTED_MODE = process.env.DOCS_RESTRICTED_MODE === 'true';
|
|
58
|
+
const DOCS_PDF_ENABLED = !DOCS_RESTRICTED_MODE;
|
|
59
|
+
const DOCS_PAGE_WATERMARK_TEXT = DOCS_TITLE;
|
|
59
60
|
const DOCS_DIR = path.resolve(process.env.DOCS_ROOT || path.join(process.cwd(), 'docs'));
|
|
60
61
|
exports.docsDir = DOCS_DIR;
|
|
61
62
|
const ROOT_DIRECTORY_ORDER = (() => {
|
|
@@ -108,8 +109,8 @@ app.locals.docsStorageKeyPrefix = DOCS_STORAGE_KEY_PREFIX;
|
|
|
108
109
|
app.locals.docsThemeDefault = DOCS_THEME_DEFAULT;
|
|
109
110
|
app.locals.docsThemeModeDefault = DOCS_THEME_MODE_DEFAULT;
|
|
110
111
|
app.locals.docsThemeEnabled = DOCS_THEME_ENABLED;
|
|
111
|
-
app.locals.
|
|
112
|
-
app.locals.
|
|
112
|
+
app.locals.docsRestrictedMode = DOCS_RESTRICTED_MODE;
|
|
113
|
+
app.locals.docsPageWatermarkText = DOCS_PAGE_WATERMARK_TEXT;
|
|
113
114
|
app.locals.docsAuthEnabled = BUILT_IN_AUTH_ENABLED;
|
|
114
115
|
app.locals.docsAuthStorageKey = AUTH_CONFIG.localStorageKey || `${DOCS_STORAGE_KEY_PREFIX}:auth-passphrase`;
|
|
115
116
|
function normalizeDocRelativePath(value) {
|
|
@@ -239,11 +240,13 @@ app.get('/favicon.ico', (req, res) => {
|
|
|
239
240
|
app.use('/mermaid', express.static(MERMAID_DIR));
|
|
240
241
|
// GitHub Markdown 样式(本地,避免 CDN 慢)
|
|
241
242
|
app.use('/vendor/github-markdown-css', express.static(MARKDOWN_THEME_DIR));
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
app.use('/vendor/
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
if (DOCS_PDF_ENABLED) {
|
|
244
|
+
// html2canvas / DOMPurify / jsPDF(本地,用于前端导出 PDF)
|
|
245
|
+
app.use('/vendor/html2canvas', express.static(HTML2CANVAS_DIR));
|
|
246
|
+
app.use('/vendor/dompurify', express.static(DOMPURIFY_DIR));
|
|
247
|
+
// jsPDF(本地,用于 Mermaid 图导出 PDF)
|
|
248
|
+
app.use('/vendor/jspdf', express.static(JSPDF_DIR));
|
|
249
|
+
}
|
|
247
250
|
app.use(express.json({ limit: '16kb' }));
|
|
248
251
|
app.use(express.urlencoded({ extended: false, limit: '16kb' }));
|
|
249
252
|
function readCookie(cookieHeader, name) {
|
|
@@ -500,8 +503,11 @@ function renderTree(tree, level = 0) {
|
|
|
500
503
|
else {
|
|
501
504
|
// 文件路径直接使用,路径重定向在 resolveDocPath 中处理
|
|
502
505
|
const url = `/doc?path=${encodeURIComponent(item.path)}`;
|
|
506
|
+
const isReadme = item.name.toLowerCase() === 'readme.md';
|
|
507
|
+
const readmeClass = isReadme ? ' readme-link' : '';
|
|
508
|
+
const readmeBadge = isReadme ? '<span class="tree-readme-badge">README</span>' : '';
|
|
503
509
|
html += `<li class="file">
|
|
504
|
-
<a href="${url}" class="file-link" title="${itemName}"><span class="tree-kind-icon tree-kind-icon-file" aria-hidden="true">📄</span>${itemName}</a>
|
|
510
|
+
<a href="${url}" class="file-link${readmeClass}" title="${itemName}"><span class="tree-kind-icon tree-kind-icon-file" aria-hidden="true">📄</span><span class="tree-file-name">${itemName}</span>${readmeBadge}</a>
|
|
505
511
|
</li>`;
|
|
506
512
|
}
|
|
507
513
|
}
|
|
@@ -41,10 +41,7 @@ export default defineConfig({
|
|
|
41
41
|
localStorageKey: 'my-project:auth-passphrase',
|
|
42
42
|
sessionTtlMinutes: 24 * 60,
|
|
43
43
|
},
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
text: '我的项目文档',
|
|
48
|
-
},
|
|
49
|
-
},
|
|
44
|
+
// 受限阅读模式;默认关闭。开启后隐藏打印/PDF、Mermaid 下载和大图查看,
|
|
45
|
+
// 并使用 site.title 显示固定水印,同时禁止页面文字选择与复制。
|
|
46
|
+
restrictedMode: false,
|
|
50
47
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docnest",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "可复用的本地 Markdown 文档中心",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -66,5 +66,6 @@
|
|
|
66
66
|
"@types/node": "^22.10.0",
|
|
67
67
|
"tsx": "^4.19.2",
|
|
68
68
|
"typescript": "^5.7.2"
|
|
69
|
-
}
|
|
69
|
+
},
|
|
70
|
+
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"
|
|
70
71
|
}
|
|
@@ -451,7 +451,9 @@ body[data-theme="dark"] .dir-toggle.active {
|
|
|
451
451
|
color: var(--linear-text);
|
|
452
452
|
text-decoration: none;
|
|
453
453
|
font-size: 0.8125rem;
|
|
454
|
-
display:
|
|
454
|
+
display: flex;
|
|
455
|
+
align-items: center;
|
|
456
|
+
gap: 0.35rem;
|
|
455
457
|
padding: 0.35rem 0.5rem;
|
|
456
458
|
border-radius: var(--linear-radius);
|
|
457
459
|
transition: background-color 0.15s ease, color 0.15s ease;
|
|
@@ -460,6 +462,40 @@ body[data-theme="dark"] .dir-toggle.active {
|
|
|
460
462
|
white-space: nowrap;
|
|
461
463
|
}
|
|
462
464
|
|
|
465
|
+
.tree-kind-icon-file {
|
|
466
|
+
flex: 0 0 auto;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.tree-file-name {
|
|
470
|
+
min-width: 0;
|
|
471
|
+
overflow: hidden;
|
|
472
|
+
text-overflow: ellipsis;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.file-link.readme-link {
|
|
476
|
+
background: rgba(94, 106, 210, 0.07);
|
|
477
|
+
font-weight: 600;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.file-link.readme-link:hover,
|
|
481
|
+
.file-link.readme-link.active {
|
|
482
|
+
background: rgba(94, 106, 210, 0.14);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.tree-readme-badge,
|
|
486
|
+
.dir-listing-item-badge {
|
|
487
|
+
flex: 0 0 auto;
|
|
488
|
+
margin-left: auto;
|
|
489
|
+
padding: 0.1rem 0.4rem;
|
|
490
|
+
border: 1px solid rgba(94, 106, 210, 0.34);
|
|
491
|
+
border-radius: 999px;
|
|
492
|
+
color: var(--linear-accent);
|
|
493
|
+
font-size: 0.625rem;
|
|
494
|
+
font-weight: 700;
|
|
495
|
+
letter-spacing: 0.04em;
|
|
496
|
+
line-height: 1.2;
|
|
497
|
+
}
|
|
498
|
+
|
|
463
499
|
.file-link:hover {
|
|
464
500
|
background-color: rgba(0, 0, 0, 0.04);
|
|
465
501
|
color: var(--linear-accent);
|
|
@@ -2230,6 +2266,7 @@ body[data-theme="dark"] .pdf-export-modal__desc {
|
|
|
2230
2266
|
}
|
|
2231
2267
|
|
|
2232
2268
|
body[data-theme="dark"] .pdf-export-modal__option,
|
|
2269
|
+
body[data-theme="dark"] .pdf-export-modal__watermark-input,
|
|
2233
2270
|
body[data-theme="dark"] .pdf-export-modal__btn {
|
|
2234
2271
|
background: #1c1c1e;
|
|
2235
2272
|
border-color: #2c2c2e;
|
|
@@ -5696,6 +5733,17 @@ body[data-theme="dark"] .breadcrumb-separator {
|
|
|
5696
5733
|
color: var(--linear-text-muted);
|
|
5697
5734
|
}
|
|
5698
5735
|
|
|
5736
|
+
.dir-listing-item.is-readme {
|
|
5737
|
+
background: rgba(94, 106, 210, 0.07);
|
|
5738
|
+
border-left: 3px solid var(--linear-accent);
|
|
5739
|
+
padding-left: calc(0.875rem - 3px);
|
|
5740
|
+
}
|
|
5741
|
+
|
|
5742
|
+
.dir-listing-item.is-readme .dir-listing-item-name {
|
|
5743
|
+
color: var(--linear-text);
|
|
5744
|
+
font-weight: 650;
|
|
5745
|
+
}
|
|
5746
|
+
|
|
5699
5747
|
.dir-listing-item.is-dir .dir-listing-item-name {
|
|
5700
5748
|
font-weight: 550;
|
|
5701
5749
|
color: var(--linear-text);
|
|
@@ -40,6 +40,58 @@ body {
|
|
|
40
40
|
font-family: var(--doc-font-ui);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/* Protected reading mode: no text selection/copy and a persistent viewport watermark. */
|
|
44
|
+
html.docnest-page-protected body,
|
|
45
|
+
html.docnest-page-protected body * {
|
|
46
|
+
-webkit-user-select: none !important;
|
|
47
|
+
-moz-user-select: none !important;
|
|
48
|
+
user-select: none !important;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
html.docnest-page-protected body input,
|
|
52
|
+
html.docnest-page-protected body textarea,
|
|
53
|
+
html.docnest-page-protected body select,
|
|
54
|
+
html.docnest-page-protected body [contenteditable="true"] {
|
|
55
|
+
-webkit-user-select: text !important;
|
|
56
|
+
-moz-user-select: text !important;
|
|
57
|
+
user-select: text !important;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Restricted mode keeps Mermaid inline but removes every affordance for opening
|
|
61
|
+
* or exporting a larger copy of the diagram. */
|
|
62
|
+
html.docnest-restricted .markdown-body .mermaid {
|
|
63
|
+
cursor: default;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
html.docnest-restricted .markdown-body .mermaid::after,
|
|
67
|
+
html.docnest-restricted .markdown-body .mermaid-download-btn {
|
|
68
|
+
display: none !important;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.docnest-page-watermark {
|
|
72
|
+
position: fixed;
|
|
73
|
+
inset: 0;
|
|
74
|
+
z-index: 9990;
|
|
75
|
+
display: grid;
|
|
76
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
77
|
+
grid-template-rows: repeat(4, minmax(0, 1fr));
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
pointer-events: none;
|
|
80
|
+
user-select: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.docnest-page-watermark__item {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
color: var(--linear-text, rgba(15, 23, 42, 0.42));
|
|
88
|
+
font: 700 clamp(0.75rem, 1.5vw, 1.5rem)/1 var(--doc-font-mono);
|
|
89
|
+
letter-spacing: 0.08em;
|
|
90
|
+
opacity: 0.12;
|
|
91
|
+
transform: rotate(-24deg);
|
|
92
|
+
white-space: nowrap;
|
|
93
|
+
}
|
|
94
|
+
|
|
43
95
|
::selection {
|
|
44
96
|
background: var(--doc-selection);
|
|
45
97
|
}
|
|
@@ -318,8 +370,7 @@ body[data-doc-theme][data-theme="dark"] .pdf-export-modal__panel {
|
|
|
318
370
|
}
|
|
319
371
|
|
|
320
372
|
body[data-doc-theme][data-theme="dark"] .download-menu-content button,
|
|
321
|
-
body[data-doc-theme][data-theme="dark"] .pdf-export-
|
|
322
|
-
body[data-doc-theme][data-theme="dark"] .pdf-export-modal__watermark-input {
|
|
373
|
+
body[data-doc-theme][data-theme="dark"] .pdf-export-modal__btn {
|
|
323
374
|
border-color: var(--linear-border);
|
|
324
375
|
background: var(--linear-bg-subtle);
|
|
325
376
|
color: var(--linear-text);
|
|
@@ -313,7 +313,7 @@
|
|
|
313
313
|
return Math.max(minBreakY - startY, searchStartY + bestRow - startY);
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
function addCanvasToPdf(pdf, canvas, margin, blockBreakpoints,
|
|
316
|
+
function addCanvasToPdf(pdf, canvas, margin, blockBreakpoints, watermark) {
|
|
317
317
|
var pageWidth = pdf.internal.pageSize.getWidth();
|
|
318
318
|
var pageHeight = pdf.internal.pageSize.getHeight();
|
|
319
319
|
var usableWidth = pageWidth - margin * 2;
|
|
@@ -357,8 +357,14 @@
|
|
|
357
357
|
sliceHeight
|
|
358
358
|
);
|
|
359
359
|
|
|
360
|
-
if (
|
|
361
|
-
drawWatermarkPattern(
|
|
360
|
+
if (watermark && watermark.enabled) {
|
|
361
|
+
drawWatermarkPattern(
|
|
362
|
+
context,
|
|
363
|
+
pageCanvas.width,
|
|
364
|
+
pageCanvas.height,
|
|
365
|
+
pageCanvasHeight,
|
|
366
|
+
watermark.text,
|
|
367
|
+
);
|
|
362
368
|
}
|
|
363
369
|
|
|
364
370
|
if (pageIndex > 0) {
|
|
@@ -413,18 +419,17 @@
|
|
|
413
419
|
function openPdfExportDialog() {
|
|
414
420
|
return new Promise(function(resolve) {
|
|
415
421
|
var config = window.DOCNEST_CONFIG || {};
|
|
416
|
-
var
|
|
417
|
-
var defaultWatermarkText = String(config.watermarkText || document.title || '');
|
|
422
|
+
var defaultWatermarkText = String(config.siteTitle || document.title || '');
|
|
418
423
|
var modalId = 'pdf-export-modal';
|
|
419
424
|
var overlay = document.createElement('div');
|
|
420
425
|
overlay.className = 'pdf-export-modal';
|
|
421
426
|
overlay.innerHTML = [
|
|
422
427
|
'<div class="pdf-export-modal__panel" role="dialog" aria-modal="true" aria-labelledby="pdf-export-title">',
|
|
423
428
|
'<div class="pdf-export-modal__title" id="pdf-export-title">下载 PDF</div>',
|
|
424
|
-
'<div class="pdf-export-modal__desc"
|
|
429
|
+
'<div class="pdf-export-modal__desc">可为本次导出的 PDF 临时添加水印,不会修改项目配置。</div>',
|
|
425
430
|
'<label class="pdf-export-modal__option">',
|
|
426
431
|
'<input type="checkbox" id="pdf-export-watermark-checkbox">',
|
|
427
|
-
'<span class="pdf-export-modal__option-text"
|
|
432
|
+
'<span class="pdf-export-modal__option-text">本次导出添加水印</span>',
|
|
428
433
|
'</label>',
|
|
429
434
|
'<label class="pdf-export-modal__watermark-field" for="pdf-export-watermark-text">水印文字</label>',
|
|
430
435
|
'<input class="pdf-export-modal__watermark-input" id="pdf-export-watermark-text" type="text" maxlength="80">',
|
|
@@ -446,45 +451,38 @@
|
|
|
446
451
|
}
|
|
447
452
|
|
|
448
453
|
overlay.addEventListener('click', function(event) {
|
|
449
|
-
if (event.target === overlay)
|
|
450
|
-
close(null);
|
|
451
|
-
}
|
|
454
|
+
if (event.target === overlay) close(null);
|
|
452
455
|
});
|
|
453
|
-
|
|
454
456
|
overlay.querySelector('[data-action="cancel"]').addEventListener('click', function() {
|
|
455
457
|
close(null);
|
|
456
458
|
});
|
|
457
|
-
|
|
458
459
|
overlay.querySelector('[data-action="confirm"]').addEventListener('click', function() {
|
|
459
460
|
var checkbox = overlay.querySelector('#pdf-export-watermark-checkbox');
|
|
460
461
|
var watermarkInput = overlay.querySelector('#pdf-export-watermark-text');
|
|
461
462
|
close({
|
|
462
|
-
|
|
463
|
-
|
|
463
|
+
enabled: !!(checkbox && checkbox.checked),
|
|
464
|
+
text: watermarkInput ? watermarkInput.value.trim() : defaultWatermarkText,
|
|
464
465
|
});
|
|
465
466
|
});
|
|
466
467
|
|
|
467
468
|
document.body.appendChild(overlay);
|
|
468
469
|
if (typeof pushModal === 'function') {
|
|
469
|
-
pushModal(modalId, function() {
|
|
470
|
-
close(null);
|
|
471
|
-
});
|
|
470
|
+
pushModal(modalId, function() { close(null); });
|
|
472
471
|
}
|
|
473
472
|
|
|
474
473
|
var checkbox = overlay.querySelector('#pdf-export-watermark-checkbox');
|
|
474
|
+
var watermarkInput = overlay.querySelector('#pdf-export-watermark-text');
|
|
475
|
+
if (watermarkInput) {
|
|
476
|
+
watermarkInput.value = defaultWatermarkText;
|
|
477
|
+
watermarkInput.disabled = true;
|
|
478
|
+
}
|
|
475
479
|
if (checkbox) {
|
|
476
|
-
checkbox.checked =
|
|
480
|
+
checkbox.checked = false;
|
|
477
481
|
checkbox.addEventListener('change', function() {
|
|
478
|
-
var watermarkInput = overlay.querySelector('#pdf-export-watermark-text');
|
|
479
482
|
if (watermarkInput) watermarkInput.disabled = !checkbox.checked;
|
|
480
483
|
});
|
|
481
484
|
checkbox.focus();
|
|
482
485
|
}
|
|
483
|
-
var watermarkInput = overlay.querySelector('#pdf-export-watermark-text');
|
|
484
|
-
if (watermarkInput) {
|
|
485
|
-
watermarkInput.value = defaultWatermarkText;
|
|
486
|
-
watermarkInput.disabled = !defaultWatermarkEnabled;
|
|
487
|
-
}
|
|
488
486
|
});
|
|
489
487
|
}
|
|
490
488
|
|
|
@@ -568,7 +566,7 @@
|
|
|
568
566
|
}
|
|
569
567
|
}
|
|
570
568
|
|
|
571
|
-
function exportCurrentDoc(button,
|
|
569
|
+
function exportCurrentDoc(button, watermark) {
|
|
572
570
|
var article = document.querySelector('.main-content .markdown-body');
|
|
573
571
|
if (!article) {
|
|
574
572
|
showPdfExportError('未找到当前文档内容,无法导出 PDF。');
|
|
@@ -588,21 +586,13 @@
|
|
|
588
586
|
format: 'a4',
|
|
589
587
|
compress: true,
|
|
590
588
|
});
|
|
591
|
-
var config = window.DOCNEST_CONFIG || {};
|
|
592
|
-
var watermarkEnabled = options && typeof options.watermarkEnabled === 'boolean'
|
|
593
|
-
? options.watermarkEnabled
|
|
594
|
-
: config.watermarkEnabled === true;
|
|
595
|
-
var watermarkText = options && options.watermarkText
|
|
596
|
-
? options.watermarkText
|
|
597
|
-
: String(config.watermarkText || document.title || '');
|
|
598
|
-
|
|
599
589
|
return waitForExportReady()
|
|
600
590
|
.then(function() {
|
|
601
591
|
return renderCanvas(exportRoot);
|
|
602
592
|
})
|
|
603
593
|
.then(function(canvas) {
|
|
604
594
|
var blockBreakpoints = collectBlockBreakpoints(exportRoot, canvas.height / Math.max(exportRoot.scrollHeight, 1));
|
|
605
|
-
addCanvasToPdf(pdf, canvas, margin, blockBreakpoints,
|
|
595
|
+
addCanvasToPdf(pdf, canvas, margin, blockBreakpoints, watermark);
|
|
606
596
|
pdf.save(getExportFileName() + '.pdf');
|
|
607
597
|
})
|
|
608
598
|
.finally(function() {
|
|
@@ -619,13 +609,13 @@
|
|
|
619
609
|
}
|
|
620
610
|
|
|
621
611
|
function init() {
|
|
612
|
+
if (window.DOCNEST_CONFIG && window.DOCNEST_CONFIG.restrictedMode === true) return;
|
|
622
613
|
var button = document.getElementById('download-doc-pdf-btn');
|
|
623
614
|
if (!button) return;
|
|
624
615
|
|
|
625
616
|
button.addEventListener('click', function() {
|
|
626
|
-
openPdfExportDialog().then(function(
|
|
627
|
-
if (
|
|
628
|
-
exportCurrentDoc(button, result);
|
|
617
|
+
openPdfExportDialog().then(function(watermark) {
|
|
618
|
+
if (watermark) exportCurrentDoc(button, watermark);
|
|
629
619
|
});
|
|
630
620
|
});
|
|
631
621
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// 图表下载和复制功能
|
|
2
|
+
function isDocNestRestrictedMode() {
|
|
3
|
+
return !!(window.DOCNEST_CONFIG && window.DOCNEST_CONFIG.restrictedMode === true);
|
|
4
|
+
}
|
|
5
|
+
|
|
2
6
|
function downloadCurrentDiagram() {
|
|
7
|
+
if (isDocNestRestrictedMode()) return;
|
|
3
8
|
if (!currentSvg) return;
|
|
4
9
|
// 获取原始 SVG(移除缩放样式)
|
|
5
10
|
const originalSvg = currentSvg.cloneNode(true);
|
|
@@ -10,6 +15,7 @@ function downloadCurrentDiagram() {
|
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
function copyCurrentDiagram() {
|
|
18
|
+
if (isDocNestRestrictedMode()) return;
|
|
13
19
|
if (!currentSvg) return;
|
|
14
20
|
|
|
15
21
|
// 获取原始 SVG(移除缩放样式)
|
|
@@ -199,6 +205,7 @@ function showCopyNotification(message, success) {
|
|
|
199
205
|
|
|
200
206
|
// 下载功能
|
|
201
207
|
function showDownloadMenu(svgElement, defaultName) {
|
|
208
|
+
if (isDocNestRestrictedMode()) return;
|
|
202
209
|
const menu = document.createElement('div');
|
|
203
210
|
menu.className = 'download-menu';
|
|
204
211
|
menu.id = 'download-menu';
|
|
@@ -246,6 +253,7 @@ function closeDownloadMenu() {
|
|
|
246
253
|
}
|
|
247
254
|
|
|
248
255
|
function downloadDiagram(format, name) {
|
|
256
|
+
if (isDocNestRestrictedMode()) return;
|
|
249
257
|
const svg = window.currentDownloadSvg;
|
|
250
258
|
if (!svg) return;
|
|
251
259
|
|
|
@@ -4,7 +4,12 @@ let defaultZoom = 1;
|
|
|
4
4
|
let currentSvg = null;
|
|
5
5
|
let isFullscreen = false;
|
|
6
6
|
|
|
7
|
+
function isDocNestRestrictedMode() {
|
|
8
|
+
return !!(window.DOCNEST_CONFIG && window.DOCNEST_CONFIG.restrictedMode === true);
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
function setupDiagramViewer() {
|
|
12
|
+
if (isDocNestRestrictedMode()) return;
|
|
8
13
|
const diagrams = document.querySelectorAll('.markdown-body .mermaid');
|
|
9
14
|
diagrams.forEach((diagram, index) => {
|
|
10
15
|
// 添加下载按钮
|
|
@@ -38,6 +43,7 @@ function setupDiagramViewer() {
|
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
function openDiagramViewer(svgElement) {
|
|
46
|
+
if (isDocNestRestrictedMode()) return;
|
|
41
47
|
const viewer = document.getElementById('diagram-viewer');
|
|
42
48
|
const container = document.getElementById('diagram-viewer-svg-container');
|
|
43
49
|
|
package/server/views/dir.ejs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<script src="/static/js/core/sidebar.js?v=<%= assetVersion %>"></script>
|
|
16
16
|
</head>
|
|
17
17
|
<body data-current-dir-path="<%= currentDirPath %>">
|
|
18
|
+
<%- include('partials/page-protection') %>
|
|
18
19
|
<div class="container">
|
|
19
20
|
<%- include('partials/header', { showBackLink: true, showPrintButton: false, showPdfButton: false }) %>
|
|
20
21
|
<div class="content">
|
|
@@ -47,9 +48,11 @@
|
|
|
47
48
|
</a>
|
|
48
49
|
<% }); %>
|
|
49
50
|
<% listing.files.forEach(function(file) { %>
|
|
50
|
-
|
|
51
|
+
<% const isReadme = file.name.toLowerCase() === 'readme.md'; %>
|
|
52
|
+
<a href="/doc?path=<%= encodeURIComponent(file.path) %>" class="dir-listing-item is-file<%= isReadme ? ' is-readme' : '' %>">
|
|
51
53
|
<span class="dir-listing-item-icon">📄</span>
|
|
52
54
|
<span class="dir-listing-item-name"><%= file.name %></span>
|
|
55
|
+
<% if (isReadme) { %><span class="dir-listing-item-badge">目录说明</span><% } %>
|
|
53
56
|
</a>
|
|
54
57
|
<% }); %>
|
|
55
58
|
</div>
|
package/server/views/doc.ejs
CHANGED
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
<script src="/static/js/core/theme.js?v=<%= assetVersion %>"></script>
|
|
16
16
|
<script src="/static/js/core/mermaid-init.js?v=<%= assetVersion %>"></script>
|
|
17
17
|
<script src="/static/js/core/doc-client-nav.js?v=<%= assetVersion %>"></script>
|
|
18
|
-
|
|
18
|
+
<% if (!docsRestrictedMode) { %>
|
|
19
|
+
<script src="/static/js/core/doc-pdf-export.js?v=<%= assetVersion %>"></script>
|
|
20
|
+
<% } %>
|
|
19
21
|
<script src="/static/js/core/sidebar.js?v=<%= assetVersion %>"></script>
|
|
20
22
|
</head>
|
|
21
23
|
<body data-current-path="<%= currentPath %>">
|
|
24
|
+
<%- include('partials/page-protection') %>
|
|
22
25
|
<div class="container">
|
|
23
26
|
<%- include('partials/header', { showBackLink: true, showPrintButton: true, showPdfButton: true }) %>
|
|
24
27
|
<div class="content">
|
|
@@ -34,26 +37,31 @@
|
|
|
34
37
|
</main>
|
|
35
38
|
</div>
|
|
36
39
|
</div>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
<% if (!docsRestrictedMode) { %>
|
|
41
|
+
<%- include('partials/diagram-viewer') %>
|
|
42
|
+
<!-- 按依赖顺序加载模块 -->
|
|
43
|
+
<script src="/static/js/utils/modal-stack.js?v=<%= assetVersion %>"></script>
|
|
44
|
+
<script src="/static/js/diagram/diagram-viewer.js?v=<%= assetVersion %>"></script>
|
|
45
|
+
<script src="/static/js/diagram/diagram-zoom.js?v=<%= assetVersion %>"></script>
|
|
46
|
+
<script src="/static/js/diagram/diagram-drag.js?v=<%= assetVersion %>"></script>
|
|
47
|
+
<script src="/static/js/diagram/diagram-controls.js?v=<%= assetVersion %>"></script>
|
|
48
|
+
<script src="/static/js/diagram/diagram-download.js?v=<%= assetVersion %>"></script>
|
|
49
|
+
<% } %>
|
|
45
50
|
<script src="/static/js/utils/app.js?v=<%= assetVersion %>"></script>
|
|
51
|
+
<% if (!docsRestrictedMode) { %>
|
|
52
|
+
<script>
|
|
53
|
+
// 打印当前文章
|
|
54
|
+
(function() {
|
|
55
|
+
const printBtn = document.getElementById('print-doc-btn');
|
|
56
|
+
if (printBtn) {
|
|
57
|
+
printBtn.addEventListener('click', function() {
|
|
58
|
+
window.print();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
})();
|
|
62
|
+
</script>
|
|
63
|
+
<% } %>
|
|
46
64
|
<script>
|
|
47
|
-
// 打印当前文章
|
|
48
|
-
(function() {
|
|
49
|
-
const printBtn = document.getElementById('print-doc-btn');
|
|
50
|
-
if (printBtn) {
|
|
51
|
-
printBtn.addEventListener('click', function() {
|
|
52
|
-
window.print();
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
})();
|
|
56
|
-
|
|
57
65
|
// 高亮当前文档;展开状态:恢复上次保存的 + 当前文档所在路径,不折叠其他已展开项
|
|
58
66
|
(function() {
|
|
59
67
|
const STORAGE_KEY = <%- JSON.stringify(docsStorageKeyPrefix + ':open-dirs') %>;
|
package/server/views/index.ejs
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
<script src="/static/js/core/sidebar.js?v=<%= assetVersion %>"></script>
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
20
|
+
<%- include('partials/page-protection') %>
|
|
20
21
|
<div class="container">
|
|
21
22
|
<%- include('partials/header', { showBackLink: false }) %>
|
|
22
23
|
<div class="content">
|
|
@@ -29,14 +30,16 @@
|
|
|
29
30
|
</main>
|
|
30
31
|
</div>
|
|
31
32
|
</div>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
<% if (!docsRestrictedMode) { %>
|
|
34
|
+
<%- include('partials/diagram-viewer') %>
|
|
35
|
+
<!-- 按依赖顺序加载模块 -->
|
|
36
|
+
<script src="/static/js/utils/modal-stack.js?v=<%= assetVersion %>"></script>
|
|
37
|
+
<script src="/static/js/diagram/diagram-viewer.js?v=<%= assetVersion %>"></script>
|
|
38
|
+
<script src="/static/js/diagram/diagram-zoom.js?v=<%= assetVersion %>"></script>
|
|
39
|
+
<script src="/static/js/diagram/diagram-drag.js?v=<%= assetVersion %>"></script>
|
|
40
|
+
<script src="/static/js/diagram/diagram-controls.js?v=<%= assetVersion %>"></script>
|
|
41
|
+
<script src="/static/js/diagram/diagram-download.js?v=<%= assetVersion %>"></script>
|
|
42
|
+
<% } %>
|
|
40
43
|
<script src="/static/js/utils/app.js?v=<%= assetVersion %>"></script>
|
|
41
44
|
<script src="/socket.io/socket.io.js"></script>
|
|
42
45
|
<script>
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
<script src="/static/js/core/sidebar.js?v=<%= assetVersion %>"></script>
|
|
15
15
|
</head>
|
|
16
16
|
<body class="not-found-page">
|
|
17
|
+
<%- include('partials/page-protection') %>
|
|
17
18
|
<div class="container">
|
|
18
19
|
<%- include('partials/header', { showBackLink: true }) %>
|
|
19
20
|
<div class="content">
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
window.DOCNEST_CONFIG = {
|
|
3
3
|
authEnabled: <%= docsAuthEnabled ? 'true' : 'false' %>,
|
|
4
4
|
authStorageKey: <%- JSON.stringify(docsAuthStorageKey) %>,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
siteTitle: <%- JSON.stringify(docsTitle) %>,
|
|
6
|
+
restrictedMode: <%= docsRestrictedMode ? 'true' : 'false' %>,
|
|
7
7
|
storageKeyPrefix: <%- JSON.stringify(docsStorageKeyPrefix) %>,
|
|
8
8
|
appearance: {
|
|
9
9
|
defaultTheme: <%- JSON.stringify(docsThemeDefault) %>,
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
</div>
|
|
14
14
|
<div class="header-meta">
|
|
15
15
|
<div class="header-actions">
|
|
16
|
-
<% if (typeof showPrintButton !== 'undefined' && showPrintButton) { %>
|
|
16
|
+
<% if (typeof showPrintButton !== 'undefined' && showPrintButton && !docsRestrictedMode) { %>
|
|
17
17
|
<button type="button" id="print-doc-btn" class="header-action-btn print-btn" title="打印当前文章">
|
|
18
18
|
<span class="print-icon">🖨️</span>
|
|
19
19
|
<span class="header-action-text">打印</span>
|
|
20
20
|
</button>
|
|
21
21
|
<% } %>
|
|
22
|
-
<% if (typeof showPdfButton !== 'undefined' && showPdfButton) { %>
|
|
22
|
+
<% if (typeof showPdfButton !== 'undefined' && showPdfButton && !docsRestrictedMode) { %>
|
|
23
23
|
<button type="button" id="download-doc-pdf-btn" class="header-action-btn download-pdf-btn" title="下载当前文章 PDF">
|
|
24
24
|
<span class="download-pdf-icon">⬇️</span>
|
|
25
25
|
<span class="header-action-text">下载 PDF</span>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<% if (docsRestrictedMode) { %>
|
|
2
|
+
<div class="docnest-page-watermark" aria-hidden="true">
|
|
3
|
+
<% for (let index = 0; index < 12; index += 1) { %>
|
|
4
|
+
<span class="docnest-page-watermark__item"><%= docsPageWatermarkText %></span>
|
|
5
|
+
<% } %>
|
|
6
|
+
</div>
|
|
7
|
+
<script>
|
|
8
|
+
document.documentElement.classList.add('docnest-page-protected', 'docnest-restricted');
|
|
9
|
+
(function() {
|
|
10
|
+
function isEditableTarget(target) {
|
|
11
|
+
return !!(target && target.closest && target.closest('input, textarea, select, [contenteditable="true"]'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function preventProtectedAction(event) {
|
|
15
|
+
if (!isEditableTarget(event.target)) event.preventDefault();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
document.addEventListener('selectstart', preventProtectedAction);
|
|
19
|
+
document.addEventListener('copy', preventProtectedAction);
|
|
20
|
+
document.addEventListener('cut', preventProtectedAction);
|
|
21
|
+
document.addEventListener('dragstart', preventProtectedAction);
|
|
22
|
+
document.addEventListener('keydown', function(event) {
|
|
23
|
+
if (isEditableTarget(event.target)) return;
|
|
24
|
+
var key = String(event.key || '').toLowerCase();
|
|
25
|
+
if ((event.metaKey || event.ctrlKey) && (key === 'a' || key === 'c' || key === 'x')) {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
})();
|
|
30
|
+
</script>
|
|
31
|
+
<% } %>
|