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 +14 -3
- package/dist/auth.d.ts +0 -2
- package/dist/auth.js +1 -18
- package/dist/cli.js +1 -2
- package/dist/config.d.ts +6 -9
- package/dist/config.js +1 -7
- package/dist/server.js +17 -37
- package/docnest.config.example.mjs +5 -8
- package/package.json +3 -2
- package/server/static/css/styles.css +50 -103
- package/server/static/css/themes.css +53 -2
- package/server/static/js/core/auth.js +0 -74
- 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 -44
- 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
|
|
|
@@ -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
|
-
|
|
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.
|
|
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) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -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-
|
|
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-
|
|
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,
|
|
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>
|
|
@@ -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
|
+
<% } %>
|