docnest 0.1.10 → 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
 
@@ -77,7 +88,7 @@ export default defineConfig({
77
88
  授权只有一道关口,没有账号、密码组合或角色体系。首次启动时,`passphrase` 会初始化到由
78
89
  `stateFile` 指定的本地授权状态文件;状态文件只保存带随机盐的口令哈希,不保存明文口令。
79
90
  按极简模式约定,前端会把可回填的口令永久保存到浏览器 `localStorage`(不写入 URL);每次打开或刷新文档页面,前端都会把当前缓存的口令交给后台重新校验。校验失败会回到口令页,校验成功才会继续打开当前页面;输入页会保留跳转前的目标地址,错误口令会停留在输入页。由于 `localStorage` 是浏览器本地明文存储,请只在受信任的浏览器环境使用此模式。
80
- `localStorageKey` 可选,用于为不同宿主项目隔离浏览器存储键。右上角的“修改口令”可以动态更新口令,修改后会清除本地缓存并使所有现有会话立即失效,必须使用新口令重新进入。
91
+ `localStorageKey` 可选,用于为不同宿主项目隔离浏览器存储键。内置授权不提供“修改口令”或“退出”按钮;需要更换口令时,停止服务、删除 `stateFile`,修改配置中的 `passphrase` 后重新启动。
81
92
  建议把 `.docnest/` 加入宿主项目的 `.gitignore`,并为口令状态文件设置仅当前用户可读写的权限。
82
93
 
83
94
  授权开启但既没有状态文件,也没有初始 `passphrase` 时,DocNest 会拒绝启动,避免意外以空口令开放文档。
package/dist/auth.d.ts CHANGED
@@ -12,7 +12,5 @@ export type DocNestAuthManager = {
12
12
  verifyPassphrase: (passphrase: string) => boolean;
13
13
  createSession: (passphrase: string) => string | null;
14
14
  authenticateSession: (token: string | undefined) => boolean;
15
- revokeSession: (token: string | undefined) => void;
16
- changePassphrase: (currentPassphrase: string, nextPassphrase: string) => boolean;
17
15
  };
18
16
  export declare function createAuthManager(config: DocNestAuthConfig): DocNestAuthManager;
package/dist/auth.js CHANGED
@@ -130,11 +130,9 @@ function createAuthManager(config) {
130
130
  verifyPassphrase: () => true,
131
131
  createSession: () => null,
132
132
  authenticateSession: () => true,
133
- revokeSession: () => undefined,
134
- changePassphrase: () => false,
135
133
  };
136
134
  }
137
- let state = readOrCreateState(config);
135
+ const state = readOrCreateState(config);
138
136
  function verifyPassphrase(passphrase) {
139
137
  return matchesHash(passphrase, state);
140
138
  }
@@ -163,19 +161,6 @@ function createAuthManager(config) {
163
161
  session.expiresAt = Date.now() + sessionMaxAgeSeconds * 1000;
164
162
  return true;
165
163
  }
166
- function revokeSession(token) {
167
- if (token)
168
- sessions.delete(token);
169
- }
170
- function changePassphrase(currentPassphrase, nextPassphrase) {
171
- if (!verifyPassphrase(currentPassphrase))
172
- return false;
173
- assertPassphrase(nextPassphrase, '新授权口令');
174
- state = createState(nextPassphrase);
175
- writeState(config.stateFile, state);
176
- sessions.clear();
177
- return true;
178
- }
179
164
  return {
180
165
  enabled,
181
166
  stateFile: config.stateFile,
@@ -184,7 +169,5 @@ function createAuthManager(config) {
184
169
  verifyPassphrase,
185
170
  createSession,
186
171
  authenticateSession,
187
- revokeSession,
188
- changePassphrase,
189
172
  };
190
173
  }
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) {
@@ -297,10 +300,7 @@ function renderLoginPage(res, nextPath, error, status = 200) {
297
300
  app.get('/login', (req, res) => {
298
301
  if (!BUILT_IN_AUTH_ENABLED)
299
302
  return res.redirect('/');
300
- const message = req.query.changed
301
- ? '口令已更新,请使用新口令重新进入。'
302
- : '';
303
- return renderLoginPage(res, req.query.next, message);
303
+ return renderLoginPage(res, req.query.next, '');
304
304
  });
305
305
  app.post('/auth/verify', (req, res) => {
306
306
  if (!BUILT_IN_AUTH_ENABLED)
@@ -323,29 +323,6 @@ app.post('/auth/login', (req, res) => {
323
323
  setAuthCookie(res, token);
324
324
  return res.redirect(nextPath);
325
325
  });
326
- app.post('/auth/logout', (req, res) => {
327
- AUTH_MANAGER.revokeSession(readAuthToken(req));
328
- clearAuthCookie(res);
329
- return res.redirect('/login');
330
- });
331
- app.post('/auth/change-password', (req, res) => {
332
- if (!BUILT_IN_AUTH_ENABLED || !AUTH_MANAGER.authenticateSession(readAuthToken(req))) {
333
- return res.status(401).json({ ok: false, error: '当前会话未授权。' });
334
- }
335
- try {
336
- const changed = AUTH_MANAGER.changePassphrase(String(req.body?.currentPassphrase || ''), String(req.body?.nextPassphrase || ''));
337
- if (!changed)
338
- return res.status(401).json({ ok: false, error: '当前口令不正确。' });
339
- clearAuthCookie(res);
340
- return res.json({ ok: true });
341
- }
342
- catch (error) {
343
- return res.status(400).json({
344
- ok: false,
345
- error: error instanceof Error ? error.message : '新口令无效。',
346
- });
347
- }
348
- });
349
326
  function documentAuthMiddleware(req, res, next) {
350
327
  if (!BUILT_IN_AUTH_ENABLED)
351
328
  return next();
@@ -526,8 +503,11 @@ function renderTree(tree, level = 0) {
526
503
  else {
527
504
  // 文件路径直接使用,路径重定向在 resolveDocPath 中处理
528
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>' : '';
529
509
  html += `<li class="file">
530
- <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>
531
511
  </li>`;
532
512
  }
533
513
  }
@@ -34,17 +34,14 @@ export default defineConfig({
34
34
  auth: {
35
35
  // 极简单口令授权;关闭时文档中心保持本地开放。
36
36
  enabled: false,
37
- // 仅首次启动且授权状态文件不存在时使用,之后可在页面内修改。
37
+ // 仅首次启动且授权状态文件不存在时使用;更换口令需手动删除状态文件后重启。
38
38
  passphrase: '',
39
39
  stateFile: '.docnest/auth.json',
40
- // 前端 localStorage 的键;口令会永久保存在浏览器本地,直到用户退出或修改口令。
40
+ // 前端 localStorage 的键;口令会永久保存在浏览器本地,直到手动清除或口令失效。
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.10",
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);
@@ -5778,43 +5826,6 @@ html.docnest-auth-pending::before {
5778
5826
  font-size: 0.9rem;
5779
5827
  }
5780
5828
 
5781
- .auth-actions {
5782
- display: inline-flex;
5783
- align-items: center;
5784
- gap: 0.4rem;
5785
- }
5786
-
5787
- .auth-logout-form {
5788
- display: inline-flex;
5789
- margin: 0;
5790
- }
5791
-
5792
- .auth-change-password-btn,
5793
- .auth-logout-btn {
5794
- white-space: nowrap;
5795
- }
5796
-
5797
- .auth-modal[hidden] {
5798
- display: none;
5799
- }
5800
-
5801
- .auth-modal {
5802
- position: fixed;
5803
- inset: 0;
5804
- z-index: 1000;
5805
- display: grid;
5806
- place-items: center;
5807
- padding: 1.25rem;
5808
- }
5809
-
5810
- .auth-modal-backdrop {
5811
- position: absolute;
5812
- inset: 0;
5813
- background: rgba(10, 16, 24, 0.42);
5814
- backdrop-filter: blur(4px);
5815
- }
5816
-
5817
- .auth-modal-card,
5818
5829
  .auth-login-card {
5819
5830
  position: relative;
5820
5831
  z-index: 1;
@@ -5825,25 +5836,11 @@ html.docnest-auth-pending::before {
5825
5836
  box-shadow: 0 24px 70px rgba(17, 29, 40, 0.2);
5826
5837
  }
5827
5838
 
5828
- .auth-modal-card {
5829
- width: min(100%, 440px);
5830
- padding: 1.5rem;
5831
- }
5832
-
5833
- .auth-modal-header {
5834
- display: flex;
5835
- align-items: flex-start;
5836
- justify-content: space-between;
5837
- gap: 1rem;
5838
- }
5839
-
5840
- .auth-modal-header h2,
5841
5839
  .auth-login-card h1 {
5842
5840
  margin: 0;
5843
5841
  color: var(--linear-text, #18212a);
5844
5842
  }
5845
5843
 
5846
- .auth-modal-eyebrow,
5847
5844
  .auth-login-eyebrow {
5848
5845
  margin: 0 0 0.35rem;
5849
5846
  color: var(--linear-accent, #267c83);
@@ -5852,35 +5849,12 @@ html.docnest-auth-pending::before {
5852
5849
  letter-spacing: 0.12em;
5853
5850
  }
5854
5851
 
5855
- .auth-modal-close {
5856
- width: 2rem;
5857
- height: 2rem;
5858
- padding: 0;
5859
- border: 0;
5860
- border-radius: 999px;
5861
- color: var(--linear-text-muted, #63727f);
5862
- background: transparent;
5863
- font-size: 1.5rem;
5864
- line-height: 1;
5865
- cursor: pointer;
5866
- }
5867
-
5868
- .auth-modal-close:hover {
5869
- background: var(--linear-bg-subtle, #eef2f5);
5870
- }
5871
-
5872
- .auth-modal-help,
5873
5852
  .auth-login-description,
5874
5853
  .auth-login-note {
5875
5854
  color: var(--linear-text-muted, #63727f);
5876
5855
  line-height: 1.6;
5877
5856
  }
5878
5857
 
5879
- .auth-modal-help {
5880
- margin: 1rem 0 1.25rem;
5881
- font-size: 0.875rem;
5882
- }
5883
-
5884
5858
  .auth-form {
5885
5859
  display: grid;
5886
5860
  gap: 0.9rem;
@@ -5920,15 +5894,7 @@ html.docnest-auth-pending::before {
5920
5894
  font-size: 0.875rem;
5921
5895
  }
5922
5896
 
5923
- .auth-modal-actions {
5924
- display: flex;
5925
- justify-content: flex-end;
5926
- gap: 0.6rem;
5927
- margin-top: 0.25rem;
5928
- }
5929
-
5930
- .auth-primary-btn,
5931
- .auth-secondary-btn {
5897
+ .auth-primary-btn {
5932
5898
  min-height: 2.5rem;
5933
5899
  padding: 0.55rem 0.95rem;
5934
5900
  border-radius: 8px;
@@ -5943,12 +5909,6 @@ html.docnest-auth-pending::before {
5943
5909
  background: var(--linear-accent, #267c83);
5944
5910
  }
5945
5911
 
5946
- .auth-secondary-btn {
5947
- border: 1px solid var(--linear-border, #d7dee5);
5948
- color: var(--linear-text, #18212a);
5949
- background: var(--linear-bg-subtle, #eef2f5);
5950
- }
5951
-
5952
5912
  .auth-page {
5953
5913
  min-height: 100vh;
5954
5914
  display: grid;
@@ -6010,16 +5970,3 @@ html.docnest-auth-pending::before {
6010
5970
  font-size: 0.75rem;
6011
5971
  text-align: center;
6012
5972
  }
6013
-
6014
- @media (max-width: 768px) {
6015
- .auth-actions {
6016
- gap: 0.25rem;
6017
- }
6018
-
6019
- .auth-change-password-btn,
6020
- .auth-logout-btn {
6021
- min-width: 44px;
6022
- padding-inline: 0.5rem;
6023
- font-size: 0.75rem;
6024
- }
6025
- }
@@ -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);
@@ -92,78 +92,6 @@
92
92
  document.documentElement.classList.remove('docnest-auth-pending');
93
93
  }
94
94
 
95
- function initChangePassword() {
96
- var modal = document.getElementById('auth-change-password-modal');
97
- var openButton = document.getElementById('auth-change-password-btn');
98
- var form = document.getElementById('auth-change-password-form');
99
- var error = document.getElementById('auth-change-password-error');
100
- if (!modal || !openButton || !form) return;
101
-
102
- var closeButtons = modal.querySelectorAll('[data-auth-close]');
103
-
104
- function setError(message) {
105
- if (!error) return;
106
- error.textContent = message || '';
107
- error.hidden = !message;
108
- }
109
-
110
- function closeModal() {
111
- modal.hidden = true;
112
- setError('');
113
- form.reset();
114
- }
115
-
116
- openButton.addEventListener('click', function () {
117
- modal.hidden = false;
118
- var firstInput = form.querySelector('input');
119
- if (firstInput) firstInput.focus();
120
- });
121
-
122
- closeButtons.forEach(function (button) {
123
- button.addEventListener('click', closeModal);
124
- });
125
-
126
- form.addEventListener('submit', async function (event) {
127
- event.preventDefault();
128
- setError('');
129
- var formData = new FormData(form);
130
- var nextPassphrase = String(formData.get('nextPassphrase') || '');
131
- var confirmPassphrase = String(formData.get('confirmPassphrase') || '');
132
- if (nextPassphrase !== confirmPassphrase) {
133
- setError('两次输入的新口令不一致。');
134
- return;
135
- }
136
-
137
- try {
138
- var response = await fetch('/auth/change-password', {
139
- method: 'POST',
140
- credentials: 'same-origin',
141
- headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
142
- body: JSON.stringify({
143
- currentPassphrase: String(formData.get('currentPassphrase') || ''),
144
- nextPassphrase: nextPassphrase,
145
- }),
146
- });
147
- var result = await response.json().catch(function () { return {}; });
148
- if (!response.ok || !result.ok) {
149
- throw new Error(result.error || '修改口令失败,请重试。');
150
- }
151
- clearPassphrase();
152
- window.location.assign('/login?changed=1');
153
- } catch (requestError) {
154
- setError(requestError instanceof Error ? requestError.message : '修改口令失败,请重试。');
155
- }
156
- });
157
- }
158
-
159
- function initLogout() {
160
- document.querySelectorAll('.auth-logout-form').forEach(function (form) {
161
- form.addEventListener('submit', function () {
162
- clearPassphrase();
163
- });
164
- });
165
- }
166
-
167
95
  function initLoginPage() {
168
96
  var form = document.querySelector('.auth-login-form');
169
97
  var error = document.querySelector('.auth-login-form .auth-form-error');
@@ -211,8 +139,6 @@
211
139
  return;
212
140
  }
213
141
  removePendingState();
214
- initChangePassword();
215
- initLogout();
216
142
  });
217
143
  }
218
144
 
@@ -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>
@@ -54,52 +54,10 @@
54
54
  <span class="theme-icon" aria-hidden="true">A</span>
55
55
  <span class="theme-text">跟随系统</span>
56
56
  </button>
57
- <% if (docsAuthEnabled) { %>
58
- <div class="auth-actions" aria-label="文档授权">
59
- <button id="auth-change-password-btn" class="header-action-btn auth-change-password-btn" type="button">
60
- 修改口令
61
- </button>
62
- <form action="/auth/logout" method="post" class="auth-logout-form">
63
- <button class="header-action-btn auth-logout-btn" type="submit">退出</button>
64
- </form>
65
- </div>
66
- <% } %>
67
57
  </div>
68
58
  </div>
69
59
  </div>
70
60
  </header>
71
61
  <% if (docsAuthEnabled) { %>
72
- <div class="auth-modal" id="auth-change-password-modal" hidden>
73
- <div class="auth-modal-backdrop" data-auth-close></div>
74
- <section class="auth-modal-card" role="dialog" aria-modal="true" aria-labelledby="auth-change-password-title">
75
- <div class="auth-modal-header">
76
- <div>
77
- <p class="auth-modal-eyebrow">DOCUMENT ACCESS</p>
78
- <h2 id="auth-change-password-title">修改授权口令</h2>
79
- </div>
80
- <button type="button" class="auth-modal-close" data-auth-close aria-label="关闭">×</button>
81
- </div>
82
- <p class="auth-modal-help">修改后所有已打开的文档会退出授权,需要使用新口令重新进入。</p>
83
- <form id="auth-change-password-form" class="auth-form">
84
- <label>
85
- 当前口令
86
- <input type="password" name="currentPassphrase" autocomplete="current-password" required>
87
- </label>
88
- <label>
89
- 新口令
90
- <input type="password" name="nextPassphrase" autocomplete="new-password" required>
91
- </label>
92
- <label>
93
- 确认新口令
94
- <input type="password" name="confirmPassphrase" autocomplete="new-password" required>
95
- </label>
96
- <p class="auth-form-error" id="auth-change-password-error" role="alert" hidden></p>
97
- <div class="auth-modal-actions">
98
- <button type="button" class="auth-secondary-btn" data-auth-close>取消</button>
99
- <button type="submit" class="auth-primary-btn">保存新口令</button>
100
- </div>
101
- </form>
102
- </section>
103
- </div>
104
62
  <script src="/static/js/core/auth.js?v=<%= typeof assetVersion !== 'undefined' ? assetVersion : 'dev' %>"></script>
105
63
  <% } %>
@@ -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
+ <% } %>