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 CHANGED
@@ -20,7 +20,7 @@ npx docnest serve
20
20
 
21
21
  ## 配置
22
22
 
23
- 复制 `docnest.config.example.mjs` 为项目根目录的 `docnest.config.mjs`,配置项目自己的标题、目录顺序、存储键前缀和 PDF 水印。
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
- 水印默认关闭。开启后,PDF 导出弹窗会读取配置文字,并允许本次导出临时修改;临时修改不会写回文件或浏览器存储。
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.DOCS_WATERMARK_ENABLED = String(config.export.watermark.enabled);
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
- export?: {
36
- watermark?: {
37
- enabled?: boolean;
38
- text?: string;
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
- export: {
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
- export: {
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 DOCS_WATERMARK_ENABLED = process.env.DOCS_WATERMARK_ENABLED === 'true';
58
- const DOCS_WATERMARK_TEXT = process.env.DOCS_WATERMARK_TEXT || DOCS_TITLE;
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.docsWatermarkEnabled = DOCS_WATERMARK_ENABLED;
112
- app.locals.docsWatermarkText = DOCS_WATERMARK_TEXT;
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
- // html2canvas / DOMPurify / jsPDF(本地,用于前端导出 PDF)
243
- app.use('/vendor/html2canvas', express.static(HTML2CANVAS_DIR));
244
- app.use('/vendor/dompurify', express.static(DOMPURIFY_DIR));
245
- // jsPDF(本地,用于 Mermaid 图导出 PDF)
246
- app.use('/vendor/jspdf', express.static(JSPDF_DIR));
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
- export: {
45
- watermark: {
46
- enabled: false,
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.11",
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: block;
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-modal__option,
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, watermarkEnabled, watermarkText) {
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 (watermarkEnabled) {
361
- drawWatermarkPattern(context, pageCanvas.width, pageCanvas.height, pageCanvasHeight, watermarkText);
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 defaultWatermarkEnabled = config.watermarkEnabled === true;
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">导出前确认本次 PDF 是否需要水印;本次修改不会写回项目配置。</div>',
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">启用水印</span>',
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
- watermarkEnabled: !!(checkbox && checkbox.checked),
463
- watermarkText: watermarkInput ? watermarkInput.value.trim() : defaultWatermarkText,
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 = defaultWatermarkEnabled;
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, options) {
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, watermarkEnabled, watermarkText);
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(result) {
627
- if (!result) return;
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
 
@@ -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
- <a href="/doc?path=<%= encodeURIComponent(file.path) %>" class="dir-listing-item is-file">
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>
@@ -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
- <script src="/static/js/core/doc-pdf-export.js?v=<%= assetVersion %>"></script>
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
- <%- include('partials/diagram-viewer') %>
38
- <!-- 按依赖顺序加载模块 -->
39
- <script src="/static/js/utils/modal-stack.js?v=<%= assetVersion %>"></script>
40
- <script src="/static/js/diagram/diagram-viewer.js?v=<%= assetVersion %>"></script>
41
- <script src="/static/js/diagram/diagram-zoom.js?v=<%= assetVersion %>"></script>
42
- <script src="/static/js/diagram/diagram-drag.js?v=<%= assetVersion %>"></script>
43
- <script src="/static/js/diagram/diagram-controls.js?v=<%= assetVersion %>"></script>
44
- <script src="/static/js/diagram/diagram-download.js?v=<%= assetVersion %>"></script>
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') %>;
@@ -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
- <%- include('partials/diagram-viewer') %>
33
- <!-- 按依赖顺序加载模块 -->
34
- <script src="/static/js/utils/modal-stack.js?v=<%= assetVersion %>"></script>
35
- <script src="/static/js/diagram/diagram-viewer.js?v=<%= assetVersion %>"></script>
36
- <script src="/static/js/diagram/diagram-zoom.js?v=<%= assetVersion %>"></script>
37
- <script src="/static/js/diagram/diagram-drag.js?v=<%= assetVersion %>"></script>
38
- <script src="/static/js/diagram/diagram-controls.js?v=<%= assetVersion %>"></script>
39
- <script src="/static/js/diagram/diagram-download.js?v=<%= assetVersion %>"></script>
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
- watermarkEnabled: <%= docsWatermarkEnabled ? 'true' : 'false' %>,
6
- watermarkText: <%- JSON.stringify(docsWatermarkText) %>,
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
+ <% } %>