docnest 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +83 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +79 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +814 -0
- package/package.json +60 -0
- package/server/favicon.ico +0 -0
- package/server/static/css/highlight.css +72 -0
- package/server/static/css/styles.css +5762 -0
- package/server/static/favicon.ico +0 -0
- package/server/static/img/404-deep-space-light.webp +0 -0
- package/server/static/img/404-deep-space.webp +0 -0
- package/server/static/js/core/doc-client-nav.js +260 -0
- package/server/static/js/core/doc-pdf-export.js +579 -0
- package/server/static/js/core/mermaid-init.js +36 -0
- package/server/static/js/core/sidebar.js +141 -0
- package/server/static/js/core/theme.js +138 -0
- package/server/static/js/diagram/diagram-controls.js +90 -0
- package/server/static/js/diagram/diagram-download.js +594 -0
- package/server/static/js/diagram/diagram-drag.js +157 -0
- package/server/static/js/diagram/diagram-viewer.js +405 -0
- package/server/static/js/diagram/diagram-zoom.js +67 -0
- package/server/static/js/utils/app.js +14 -0
- package/server/static/js/utils/modal-stack.js +33 -0
- package/server/views/dir.ejs +142 -0
- package/server/views/doc.ejs +483 -0
- package/server/views/index.ejs +77 -0
- package/server/views/not-found.ejs +57 -0
- package/server/views/partials/breadcrumbs.ejs +15 -0
- package/server/views/partials/diagram-viewer.ejs +16 -0
- package/server/views/partials/header.ejs +37 -0
- package/server/views/partials/sidebar.ejs +19 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
+
<title><%= dirName %> - <%= docsTitle %></title>
|
|
7
|
+
<link rel="icon" href="/favicon.ico?v=<%= assetVersion %>" sizes="any">
|
|
8
|
+
<link id="markdown-theme" rel="stylesheet" href="/vendor/github-markdown-css/github-markdown-light.css?v=<%= assetVersion %>">
|
|
9
|
+
<link rel="stylesheet" href="/static/css/styles.css?v=<%= assetVersion %>">
|
|
10
|
+
<link rel="stylesheet" href="/static/css/highlight.css?v=<%= assetVersion %>">
|
|
11
|
+
<script src="/mermaid/mermaid.min.js?v=<%= assetVersion %>"></script>
|
|
12
|
+
<script src="/static/js/core/theme.js?v=<%= assetVersion %>"></script>
|
|
13
|
+
<script src="/static/js/core/sidebar.js?v=<%= assetVersion %>"></script>
|
|
14
|
+
</head>
|
|
15
|
+
<body data-current-dir-path="<%= currentDirPath %>">
|
|
16
|
+
<div class="container">
|
|
17
|
+
<%- include('partials/header', { showBackLink: true, showPrintButton: false, showPdfButton: false }) %>
|
|
18
|
+
<div class="content">
|
|
19
|
+
<%- include('partials/sidebar') %>
|
|
20
|
+
<main class="main-content">
|
|
21
|
+
<%- include('partials/breadcrumbs') %>
|
|
22
|
+
<div class="dir-listing-page">
|
|
23
|
+
<div class="dir-listing-header">
|
|
24
|
+
<span class="dir-listing-header-icon">📁</span>
|
|
25
|
+
<h2 class="dir-listing-title"><%= dirName %></h2>
|
|
26
|
+
<% if (listing.dirs.length === 0 && listing.files.length === 0) { %>
|
|
27
|
+
<span class="dir-listing-empty-tag">空目录</span>
|
|
28
|
+
<% } else { %>
|
|
29
|
+
<span class="dir-listing-count"><%= listing.dirs.length + listing.files.length %> 项</span>
|
|
30
|
+
<% } %>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<% if (listing.dirs.length === 0 && listing.files.length === 0) { %>
|
|
34
|
+
<div class="dir-listing-empty">
|
|
35
|
+
<div class="dir-listing-empty-icon">📭</div>
|
|
36
|
+
<p>此目录下暂无内容</p>
|
|
37
|
+
</div>
|
|
38
|
+
<% } else { %>
|
|
39
|
+
<div class="dir-listing-grid">
|
|
40
|
+
<% listing.dirs.forEach(function(dir) { %>
|
|
41
|
+
<a href="/dir?path=<%= encodeURIComponent(dir.path) %>" class="dir-listing-item is-dir">
|
|
42
|
+
<span class="dir-listing-item-icon">📁</span>
|
|
43
|
+
<span class="dir-listing-item-name"><%= dir.name %></span>
|
|
44
|
+
<span class="dir-listing-item-arrow">→</span>
|
|
45
|
+
</a>
|
|
46
|
+
<% }); %>
|
|
47
|
+
<% listing.files.forEach(function(file) { %>
|
|
48
|
+
<a href="/doc?path=<%= encodeURIComponent(file.path) %>" class="dir-listing-item is-file">
|
|
49
|
+
<span class="dir-listing-item-icon">📄</span>
|
|
50
|
+
<span class="dir-listing-item-name"><%= file.name %></span>
|
|
51
|
+
</a>
|
|
52
|
+
<% }); %>
|
|
53
|
+
</div>
|
|
54
|
+
<% } %>
|
|
55
|
+
</div>
|
|
56
|
+
</main>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
61
|
+
<script>
|
|
62
|
+
// 高亮左侧菜单中的当前目录节点
|
|
63
|
+
(function() {
|
|
64
|
+
var STORAGE_KEY = <%- JSON.stringify(docsStorageKeyPrefix + ':open-dirs') %>;
|
|
65
|
+
var currentDirRaw = '<%= currentDirPath %>';
|
|
66
|
+
var sidebar = document.getElementById('sidebar');
|
|
67
|
+
var treeNav = sidebar ? sidebar.querySelector('.tree-nav') : null;
|
|
68
|
+
if (!treeNav || !currentDirRaw) return;
|
|
69
|
+
|
|
70
|
+
function getDirPath(li) {
|
|
71
|
+
try {
|
|
72
|
+
var p = li.getAttribute('data-dir-path');
|
|
73
|
+
return (p ? decodeURIComponent(p) : '').replace(/\\/g, '/').trim();
|
|
74
|
+
} catch (e) { return ''; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ancestorDirPaths(dirPath) {
|
|
78
|
+
var parts = dirPath.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
79
|
+
var list = [];
|
|
80
|
+
for (var i = 1; i <= parts.length; i++) list.push(parts.slice(0, i).join('/'));
|
|
81
|
+
return list;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var ancestors = ancestorDirPaths(currentDirRaw);
|
|
85
|
+
|
|
86
|
+
// 展开祖先路径
|
|
87
|
+
var saved = [];
|
|
88
|
+
try {
|
|
89
|
+
var raw = sessionStorage.getItem(STORAGE_KEY);
|
|
90
|
+
if (raw) saved = JSON.parse(raw);
|
|
91
|
+
} catch (e) {}
|
|
92
|
+
var toExpand = new Set(saved);
|
|
93
|
+
ancestors.forEach(function(p) { toExpand.add(p); });
|
|
94
|
+
|
|
95
|
+
treeNav.querySelectorAll('.directory').forEach(function(li) {
|
|
96
|
+
var children = li.querySelector('.dir-children');
|
|
97
|
+
if (!children) return;
|
|
98
|
+
var dirPath = getDirPath(li);
|
|
99
|
+
var open = toExpand.has(dirPath);
|
|
100
|
+
children.style.display = open ? 'block' : 'none';
|
|
101
|
+
li.classList.toggle('collapsed', !open);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
try { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(toExpand))); } catch (e) {}
|
|
105
|
+
|
|
106
|
+
// 高亮匹配的目录节点:移除所有 file-link active,给匹配的 dir-toggle 加 active
|
|
107
|
+
treeNav.querySelectorAll('.file-link.active').forEach(function(link) {
|
|
108
|
+
link.classList.remove('active');
|
|
109
|
+
});
|
|
110
|
+
treeNav.querySelectorAll('.dir-toggle').forEach(function(toggle) {
|
|
111
|
+
toggle.classList.remove('active');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
treeNav.querySelectorAll('.directory').forEach(function(li) {
|
|
115
|
+
var dirPath = getDirPath(li);
|
|
116
|
+
if (dirPath === currentDirRaw) {
|
|
117
|
+
var toggle = li.querySelector('.dir-toggle');
|
|
118
|
+
if (toggle) toggle.classList.add('active');
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// 展开当前目录
|
|
123
|
+
treeNav.querySelectorAll('.directory').forEach(function(li) {
|
|
124
|
+
var dirPath = getDirPath(li);
|
|
125
|
+
if (ancestors.indexOf(dirPath) !== -1 || dirPath === currentDirRaw) {
|
|
126
|
+
var children = li.querySelector('.dir-children');
|
|
127
|
+
if (children) { children.style.display = 'block'; li.classList.remove('collapsed'); }
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// 实时更新(Socket 监听)
|
|
132
|
+
var socket = io();
|
|
133
|
+
socket.on('file-added', function() { location.reload(); });
|
|
134
|
+
socket.on('file-deleted', function() { location.reload(); });
|
|
135
|
+
socket.on('file-changed', function(data) {
|
|
136
|
+
// 目录结构变化时刷新
|
|
137
|
+
location.reload();
|
|
138
|
+
});
|
|
139
|
+
})();
|
|
140
|
+
</script>
|
|
141
|
+
</body>
|
|
142
|
+
</html>
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
+
<title><%= fileName %> - <%= docsTitle %></title>
|
|
7
|
+
<link rel="icon" href="/favicon.ico?v=<%= assetVersion %>" sizes="any">
|
|
8
|
+
<link id="markdown-theme" rel="stylesheet" href="/vendor/github-markdown-css/github-markdown-light.css?v=<%= assetVersion %>">
|
|
9
|
+
<link rel="stylesheet" href="/static/css/styles.css?v=<%= assetVersion %>">
|
|
10
|
+
<link rel="stylesheet" href="/static/css/highlight.css?v=<%= assetVersion %>">
|
|
11
|
+
<script src="/mermaid/mermaid.min.js?v=<%= assetVersion %>"></script>
|
|
12
|
+
<script>
|
|
13
|
+
window.DOCNEST_CONFIG = {
|
|
14
|
+
watermarkEnabled: <%= docsWatermarkEnabled ? 'true' : 'false' %>,
|
|
15
|
+
watermarkText: <%- JSON.stringify(docsWatermarkText) %>
|
|
16
|
+
};
|
|
17
|
+
</script>
|
|
18
|
+
<!-- 基础功能模块 -->
|
|
19
|
+
<script src="/static/js/core/theme.js?v=<%= assetVersion %>"></script>
|
|
20
|
+
<script src="/static/js/core/mermaid-init.js?v=<%= assetVersion %>"></script>
|
|
21
|
+
<script src="/static/js/core/doc-client-nav.js?v=<%= assetVersion %>"></script>
|
|
22
|
+
<script src="/static/js/core/doc-pdf-export.js?v=<%= assetVersion %>"></script>
|
|
23
|
+
<script src="/static/js/core/sidebar.js?v=<%= assetVersion %>"></script>
|
|
24
|
+
</head>
|
|
25
|
+
<body data-current-path="<%= currentPath %>">
|
|
26
|
+
<div class="container">
|
|
27
|
+
<%- include('partials/header', { showBackLink: true, showPrintButton: true, showPdfButton: true }) %>
|
|
28
|
+
<div class="content">
|
|
29
|
+
<%- include('partials/sidebar') %>
|
|
30
|
+
<main class="main-content">
|
|
31
|
+
<%- include('partials/breadcrumbs') %>
|
|
32
|
+
<div class="doc-layout">
|
|
33
|
+
<aside class="doc-toc" id="doc-toc" aria-label="文档章节导航"></aside>
|
|
34
|
+
<article class="markdown-body">
|
|
35
|
+
<%- content %>
|
|
36
|
+
</article>
|
|
37
|
+
</div>
|
|
38
|
+
</main>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
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
|
+
<script src="/static/js/utils/app.js?v=<%= assetVersion %>"></script>
|
|
50
|
+
<script>
|
|
51
|
+
// 打印当前文章
|
|
52
|
+
(function() {
|
|
53
|
+
const printBtn = document.getElementById('print-doc-btn');
|
|
54
|
+
if (printBtn) {
|
|
55
|
+
printBtn.addEventListener('click', function() {
|
|
56
|
+
window.print();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
|
|
61
|
+
// 高亮当前文档;展开状态:恢复上次保存的 + 当前文档所在路径,不折叠其他已展开项
|
|
62
|
+
(function() {
|
|
63
|
+
const STORAGE_KEY = <%- JSON.stringify(docsStorageKeyPrefix + ':open-dirs') %>;
|
|
64
|
+
const currentPathRaw = '<%= currentPath %>';
|
|
65
|
+
const sidebar = document.getElementById('sidebar');
|
|
66
|
+
const treeNav = sidebar?.querySelector('.tree-nav');
|
|
67
|
+
let activeLink = null;
|
|
68
|
+
function pathNorm(p) {
|
|
69
|
+
return (p || '').trim().replace(/\.(html|md)$/i, '').replace(/\\/g, '/');
|
|
70
|
+
}
|
|
71
|
+
function pathFromHref(href) {
|
|
72
|
+
if (!href) return '';
|
|
73
|
+
try {
|
|
74
|
+
var u = new URL(href, window.location.origin);
|
|
75
|
+
if (u.searchParams.has('path')) return u.searchParams.get('path') || '';
|
|
76
|
+
var idx = u.pathname.indexOf('/doc/');
|
|
77
|
+
if (idx !== -1) return u.pathname.slice(idx + 5).replace(/\.html$/i, '');
|
|
78
|
+
} catch (e) {}
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
function ancestorDirPaths(filePath) {
|
|
82
|
+
var norm = pathNorm(filePath);
|
|
83
|
+
var parts = norm.split('/').filter(Boolean);
|
|
84
|
+
if (parts.length <= 1) return [];
|
|
85
|
+
var list = [];
|
|
86
|
+
for (var i = 1; i < parts.length; i++) list.push(parts.slice(0, i).join('/'));
|
|
87
|
+
return list;
|
|
88
|
+
}
|
|
89
|
+
function getDirPath(li) {
|
|
90
|
+
try {
|
|
91
|
+
var p = li.getAttribute('data-dir-path');
|
|
92
|
+
return (p ? decodeURIComponent(p) : '').replace(/\\/g, '/').trim();
|
|
93
|
+
} catch (e) { return ''; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
var currentNorm = pathNorm(currentPathRaw);
|
|
97
|
+
if (treeNav) {
|
|
98
|
+
var saved = [];
|
|
99
|
+
try {
|
|
100
|
+
var raw = sessionStorage.getItem(STORAGE_KEY);
|
|
101
|
+
if (raw) saved = JSON.parse(raw);
|
|
102
|
+
} catch (e) {}
|
|
103
|
+
var toExpand = new Set(saved);
|
|
104
|
+
ancestorDirPaths(currentNorm).forEach(function(p) { toExpand.add(p); });
|
|
105
|
+
|
|
106
|
+
treeNav.querySelectorAll('.directory').forEach(function(li) {
|
|
107
|
+
var children = li.querySelector('.dir-children');
|
|
108
|
+
if (!children) return;
|
|
109
|
+
var dirPath = getDirPath(li);
|
|
110
|
+
var open = toExpand.has(dirPath);
|
|
111
|
+
children.style.display = open ? 'block' : 'none';
|
|
112
|
+
li.classList.toggle('collapsed', !open);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(toExpand)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
document.querySelectorAll('.file-link').forEach(function(link) {
|
|
119
|
+
var linkPath = pathFromHref(link.getAttribute('href') || link.href);
|
|
120
|
+
if (pathNorm(linkPath) === currentNorm) {
|
|
121
|
+
link.classList.add('active');
|
|
122
|
+
activeLink = link;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (treeNav && activeLink) {
|
|
127
|
+
var linkRect = activeLink.getBoundingClientRect();
|
|
128
|
+
var navRect = treeNav.getBoundingClientRect();
|
|
129
|
+
var isInView = linkRect.top >= navRect.top && linkRect.bottom <= navRect.bottom;
|
|
130
|
+
if (!isInView) {
|
|
131
|
+
treeNav.scrollTop = activeLink.offsetTop - 10;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
window.saveTreeOpenState = function() {
|
|
136
|
+
if (!treeNav) return;
|
|
137
|
+
var open = [];
|
|
138
|
+
treeNav.querySelectorAll('.directory').forEach(function(li) {
|
|
139
|
+
if (li.classList.contains('collapsed')) return;
|
|
140
|
+
var p = getDirPath(li);
|
|
141
|
+
if (p) open.push(p);
|
|
142
|
+
});
|
|
143
|
+
try { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(open)); } catch (e) {}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function setActiveTocLinkById(id) {
|
|
147
|
+
const tocEl = document.getElementById('doc-toc');
|
|
148
|
+
if (!tocEl || !id) return;
|
|
149
|
+
const links = Array.from(tocEl.querySelectorAll('a'));
|
|
150
|
+
links.forEach(function(link) {
|
|
151
|
+
link.classList.remove('is-active');
|
|
152
|
+
});
|
|
153
|
+
tocEl.querySelectorAll('li').forEach(function(li) {
|
|
154
|
+
li.classList.remove('is-active-parent');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const targetLink = links.find(function(link) {
|
|
158
|
+
const href = link.getAttribute('href') || '';
|
|
159
|
+
if (!href.startsWith('#')) return false;
|
|
160
|
+
return decodeURIComponent(href.slice(1)) === id;
|
|
161
|
+
});
|
|
162
|
+
if (!targetLink) return;
|
|
163
|
+
|
|
164
|
+
targetLink.classList.add('is-active');
|
|
165
|
+
let parentLi = targetLink.closest('li');
|
|
166
|
+
while (parentLi) {
|
|
167
|
+
parentLi.classList.add('is-active-parent');
|
|
168
|
+
const parentUl = parentLi.parentElement;
|
|
169
|
+
if (!parentUl) break;
|
|
170
|
+
parentLi = parentUl.closest('li');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getOrCreateSubList(li) {
|
|
175
|
+
const existing = Array.from(li.children).find(function(child) {
|
|
176
|
+
return child.tagName === 'UL' && child.classList.contains('doc-toc-sublist');
|
|
177
|
+
});
|
|
178
|
+
if (existing) return existing;
|
|
179
|
+
const sub = document.createElement('ul');
|
|
180
|
+
sub.className = 'doc-toc-sublist';
|
|
181
|
+
li.appendChild(sub);
|
|
182
|
+
return sub;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildDocToc() {
|
|
186
|
+
const tocEl = document.getElementById('doc-toc');
|
|
187
|
+
const articleEl = document.querySelector('.markdown-body');
|
|
188
|
+
if (!tocEl || !articleEl) return;
|
|
189
|
+
|
|
190
|
+
const headings = Array.from(articleEl.querySelectorAll('h2, h3, h4'));
|
|
191
|
+
if (headings.length === 0) {
|
|
192
|
+
tocEl.style.display = 'none';
|
|
193
|
+
tocEl.innerHTML = '';
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
tocEl.style.display = '';
|
|
198
|
+
const title = document.createElement('div');
|
|
199
|
+
title.className = 'doc-toc-title';
|
|
200
|
+
title.textContent = '本页导航';
|
|
201
|
+
|
|
202
|
+
const rootList = document.createElement('ul');
|
|
203
|
+
rootList.className = 'doc-toc-list toc-root';
|
|
204
|
+
const stack = [{ level: 1, list: rootList }];
|
|
205
|
+
|
|
206
|
+
headings.forEach(function(h, idx) {
|
|
207
|
+
if (!h.id) h.id = 'section-' + (idx + 1);
|
|
208
|
+
|
|
209
|
+
let level = Number(h.tagName.slice(1));
|
|
210
|
+
if (!Number.isFinite(level) || level < 2) level = 2;
|
|
211
|
+
if (level > 4) level = 4;
|
|
212
|
+
|
|
213
|
+
while (stack.length > 1 && stack[stack.length - 1].level >= level) {
|
|
214
|
+
stack.pop();
|
|
215
|
+
}
|
|
216
|
+
const parent = stack[stack.length - 1];
|
|
217
|
+
if (level > parent.level + 1) {
|
|
218
|
+
level = parent.level + 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const li = document.createElement('li');
|
|
222
|
+
li.className = 'toc-lv' + level;
|
|
223
|
+
const a = document.createElement('a');
|
|
224
|
+
a.href = '#' + encodeURIComponent(h.id);
|
|
225
|
+
a.textContent = (h.textContent || '').trim();
|
|
226
|
+
li.appendChild(a);
|
|
227
|
+
parent.list.appendChild(li);
|
|
228
|
+
|
|
229
|
+
stack.push({
|
|
230
|
+
level: level,
|
|
231
|
+
list: getOrCreateSubList(li),
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
tocEl.innerHTML = '';
|
|
236
|
+
tocEl.appendChild(title);
|
|
237
|
+
tocEl.appendChild(rootList);
|
|
238
|
+
if (window.location.hash && window.location.hash.length > 1) {
|
|
239
|
+
setActiveTocLinkById(decodeURIComponent(window.location.hash.slice(1)));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
tocEl.onclick = function(e) {
|
|
243
|
+
const link = e.target.closest('a');
|
|
244
|
+
if (!link) return;
|
|
245
|
+
const hash = link.getAttribute('href') || '';
|
|
246
|
+
if (!hash.startsWith('#')) return;
|
|
247
|
+
const id = decodeURIComponent(hash.slice(1));
|
|
248
|
+
const target = document.getElementById(id);
|
|
249
|
+
if (!target) return;
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
252
|
+
const base = window.location.pathname + window.location.search;
|
|
253
|
+
history.replaceState(history.state, '', base + '#' + encodeURIComponent(id));
|
|
254
|
+
setActiveTocLinkById(id);
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function scrollToDocHash() {
|
|
259
|
+
const rawHash = window.location.hash;
|
|
260
|
+
if (!rawHash || rawHash.length <= 1) return;
|
|
261
|
+
const id = decodeURIComponent(rawHash.slice(1));
|
|
262
|
+
const target = document.getElementById(id);
|
|
263
|
+
if (!target) return;
|
|
264
|
+
requestAnimationFrame(function() {
|
|
265
|
+
target.scrollIntoView({ behavior: 'auto', block: 'start' });
|
|
266
|
+
});
|
|
267
|
+
setActiveTocLinkById(id);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
window.buildDocToc = buildDocToc;
|
|
271
|
+
window.scrollToDocHash = scrollToDocHash;
|
|
272
|
+
window.addEventListener('hashchange', scrollToDocHash);
|
|
273
|
+
buildDocToc();
|
|
274
|
+
scrollToDocHash();
|
|
275
|
+
|
|
276
|
+
// 监听右侧文档内容区域的链接点击事件
|
|
277
|
+
const mainContent = document.querySelector('.main-content');
|
|
278
|
+
if (mainContent && treeNav && sidebar) {
|
|
279
|
+
mainContent.addEventListener('click', function(e) {
|
|
280
|
+
const link = e.target.closest('a');
|
|
281
|
+
if (!link) return;
|
|
282
|
+
|
|
283
|
+
// 检查点击是否发生在左侧菜单栏(如果是,不做任何处理)
|
|
284
|
+
if (sidebar.contains(link)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 检查是否是文档系统内的链接
|
|
289
|
+
const href = link.getAttribute('href');
|
|
290
|
+
if (!href) return;
|
|
291
|
+
|
|
292
|
+
// 判断是否是文档系统内的链接(排除外部链接、锚点、目录页链接)
|
|
293
|
+
const isDocLink = (href.startsWith('/doc?') ||
|
|
294
|
+
href.startsWith('/doc/') ||
|
|
295
|
+
href.startsWith('./') ||
|
|
296
|
+
href.startsWith('../') ||
|
|
297
|
+
(href.startsWith('/') && !href.startsWith('//') && !href.startsWith('/http'))) &&
|
|
298
|
+
!href.startsWith('#') &&
|
|
299
|
+
!href.startsWith('/dir') &&
|
|
300
|
+
!href.startsWith('http://') &&
|
|
301
|
+
!href.startsWith('https://') &&
|
|
302
|
+
!href.startsWith('mailto:');
|
|
303
|
+
|
|
304
|
+
if (!isDocLink) return;
|
|
305
|
+
|
|
306
|
+
// 从链接中提取路径信息
|
|
307
|
+
let targetPath = '';
|
|
308
|
+
try {
|
|
309
|
+
if (href.startsWith('/doc?')) {
|
|
310
|
+
// 格式:/doc?path=xxx
|
|
311
|
+
const url = new URL(href, window.location.origin);
|
|
312
|
+
targetPath = url.searchParams.get('path') || '';
|
|
313
|
+
} else if (href.startsWith('/doc/')) {
|
|
314
|
+
targetPath = href.replace('/doc/', '');
|
|
315
|
+
} else if (href.startsWith('./') || href.startsWith('../')) {
|
|
316
|
+
// 相对路径,需要解析
|
|
317
|
+
const currentDir = currentNorm.includes('/') ? currentNorm.substring(0, currentNorm.lastIndexOf('/') + 1) + '/' : '';
|
|
318
|
+
const baseUrl = window.location.origin + '/doc/' + currentDir;
|
|
319
|
+
const resolvedUrl = new URL(href, baseUrl);
|
|
320
|
+
if (resolvedUrl.pathname.startsWith('/doc/')) {
|
|
321
|
+
targetPath = resolvedUrl.pathname.replace('/doc/', '');
|
|
322
|
+
} else if (resolvedUrl.searchParams.has('path')) {
|
|
323
|
+
targetPath = resolvedUrl.searchParams.get('path');
|
|
324
|
+
}
|
|
325
|
+
} else if (href.startsWith('/')) {
|
|
326
|
+
// 绝对路径,可能是 /doc/xxx 或 /doc?path=xxx
|
|
327
|
+
if (href.includes('?')) {
|
|
328
|
+
const url = new URL(href, window.location.origin);
|
|
329
|
+
targetPath = url.searchParams.get('path') || '';
|
|
330
|
+
} else {
|
|
331
|
+
targetPath = href.replace(/^\/doc\//, '').replace(/^\//, '');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.warn('解析链接路径失败:', href, err);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!targetPath) return;
|
|
340
|
+
|
|
341
|
+
var targetNorm = pathNorm(targetPath);
|
|
342
|
+
if (targetNorm !== currentNorm && typeof window.navigateToDoc === 'function') {
|
|
343
|
+
e.preventDefault();
|
|
344
|
+
var docUrl = link.href || (window.location.origin + (href.indexOf('/') === 0 ? href : '/doc?path=' + encodeURIComponent(targetPath)));
|
|
345
|
+
window.navigateToDoc(docUrl);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
var targetLink = Array.from(document.querySelectorAll('.file-link')).find(function(sidebarLink) {
|
|
350
|
+
var linkPath = pathFromHref(sidebarLink.getAttribute('href') || sidebarLink.href);
|
|
351
|
+
return pathNorm(linkPath) === targetNorm;
|
|
352
|
+
});
|
|
353
|
+
if (!targetLink) return;
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
var linkRect = targetLink.getBoundingClientRect();
|
|
356
|
+
var navRect = treeNav.getBoundingClientRect();
|
|
357
|
+
var isInViewport = linkRect.top >= navRect.top && linkRect.bottom <= navRect.bottom;
|
|
358
|
+
if (!isInViewport) {
|
|
359
|
+
treeNav.scrollTop = targetLink.offsetTop - 10;
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
})();
|
|
364
|
+
</script>
|
|
365
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
366
|
+
<script>
|
|
367
|
+
// 实时更新功能
|
|
368
|
+
(function() {
|
|
369
|
+
const socket = io();
|
|
370
|
+
const currentPath = '<%= currentPath %>';
|
|
371
|
+
const article = document.querySelector('.markdown-body');
|
|
372
|
+
|
|
373
|
+
socket.on('file-changed', function(data) {
|
|
374
|
+
// 如果是当前查看的文档,直接更新内容
|
|
375
|
+
if (data.path === currentPath) {
|
|
376
|
+
console.log('当前文档已更新,正在刷新内容...');
|
|
377
|
+
article.innerHTML = data.content;
|
|
378
|
+
if (typeof window.buildDocToc === 'function') window.buildDocToc();
|
|
379
|
+
if (typeof window.scrollToDocHash === 'function') window.scrollToDocHash();
|
|
380
|
+
|
|
381
|
+
// 重新初始化 Mermaid
|
|
382
|
+
if (typeof mermaid !== 'undefined') {
|
|
383
|
+
mermaid.run().then(() => {
|
|
384
|
+
setupDiagramViewer();
|
|
385
|
+
});
|
|
386
|
+
} else {
|
|
387
|
+
// 如果 Mermaid 未加载,也尝试设置下载按钮(可能已有渲染好的 SVG)
|
|
388
|
+
setTimeout(() => {
|
|
389
|
+
setupDiagramViewer();
|
|
390
|
+
}, 100);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 显示更新提示
|
|
394
|
+
showUpdateNotification('文档已自动更新');
|
|
395
|
+
} else {
|
|
396
|
+
// 其他文件更新,刷新侧边栏
|
|
397
|
+
console.log('其他文件已更新:', data.path);
|
|
398
|
+
location.reload();
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
socket.on('file-added', function(data) {
|
|
403
|
+
console.log('新文件已添加:', data.path);
|
|
404
|
+
location.reload();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
socket.on('file-deleted', function(data) {
|
|
408
|
+
console.log('文件已删除:', data.path);
|
|
409
|
+
// 如果删除的是当前文档,跳转到首页
|
|
410
|
+
if (data.path === currentPath) {
|
|
411
|
+
showUpdateNotification('当前文档已删除,正在跳转...');
|
|
412
|
+
setTimeout(() => {
|
|
413
|
+
location.href = '/';
|
|
414
|
+
}, 1500);
|
|
415
|
+
} else {
|
|
416
|
+
location.reload();
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
socket.on('connect', function() {
|
|
421
|
+
console.log('✅ 已连接到服务器,实时更新已启用');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
socket.on('disconnect', function() {
|
|
425
|
+
console.log('❌ 与服务器断开连接');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// 显示更新通知
|
|
429
|
+
function showUpdateNotification(message) {
|
|
430
|
+
const notification = document.createElement('div');
|
|
431
|
+
notification.style.cssText = `
|
|
432
|
+
position: fixed;
|
|
433
|
+
top: 20px;
|
|
434
|
+
right: 20px;
|
|
435
|
+
background: #4CAF50;
|
|
436
|
+
color: white;
|
|
437
|
+
padding: 12px 24px;
|
|
438
|
+
border-radius: 4px;
|
|
439
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
440
|
+
z-index: 10000;
|
|
441
|
+
font-size: 14px;
|
|
442
|
+
animation: slideIn 0.3s ease-out;
|
|
443
|
+
`;
|
|
444
|
+
notification.textContent = '✨ ' + message;
|
|
445
|
+
document.body.appendChild(notification);
|
|
446
|
+
|
|
447
|
+
setTimeout(() => {
|
|
448
|
+
notification.style.animation = 'slideOut 0.3s ease-out';
|
|
449
|
+
setTimeout(() => {
|
|
450
|
+
document.body.removeChild(notification);
|
|
451
|
+
}, 300);
|
|
452
|
+
}, 2000);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 添加动画样式
|
|
456
|
+
const style = document.createElement('style');
|
|
457
|
+
style.textContent = `
|
|
458
|
+
@keyframes slideIn {
|
|
459
|
+
from {
|
|
460
|
+
transform: translateX(100%);
|
|
461
|
+
opacity: 0;
|
|
462
|
+
}
|
|
463
|
+
to {
|
|
464
|
+
transform: translateX(0);
|
|
465
|
+
opacity: 1;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
@keyframes slideOut {
|
|
469
|
+
from {
|
|
470
|
+
transform: translateX(0);
|
|
471
|
+
opacity: 1;
|
|
472
|
+
}
|
|
473
|
+
to {
|
|
474
|
+
transform: translateX(100%);
|
|
475
|
+
opacity: 0;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
`;
|
|
479
|
+
document.head.appendChild(style);
|
|
480
|
+
})();
|
|
481
|
+
</script>
|
|
482
|
+
</body>
|
|
483
|
+
</html>
|