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.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +83 -0
  5. package/dist/config.d.ts +36 -0
  6. package/dist/config.js +79 -0
  7. package/dist/server.d.ts +20 -0
  8. package/dist/server.js +814 -0
  9. package/package.json +60 -0
  10. package/server/favicon.ico +0 -0
  11. package/server/static/css/highlight.css +72 -0
  12. package/server/static/css/styles.css +5762 -0
  13. package/server/static/favicon.ico +0 -0
  14. package/server/static/img/404-deep-space-light.webp +0 -0
  15. package/server/static/img/404-deep-space.webp +0 -0
  16. package/server/static/js/core/doc-client-nav.js +260 -0
  17. package/server/static/js/core/doc-pdf-export.js +579 -0
  18. package/server/static/js/core/mermaid-init.js +36 -0
  19. package/server/static/js/core/sidebar.js +141 -0
  20. package/server/static/js/core/theme.js +138 -0
  21. package/server/static/js/diagram/diagram-controls.js +90 -0
  22. package/server/static/js/diagram/diagram-download.js +594 -0
  23. package/server/static/js/diagram/diagram-drag.js +157 -0
  24. package/server/static/js/diagram/diagram-viewer.js +405 -0
  25. package/server/static/js/diagram/diagram-zoom.js +67 -0
  26. package/server/static/js/utils/app.js +14 -0
  27. package/server/static/js/utils/modal-stack.js +33 -0
  28. package/server/views/dir.ejs +142 -0
  29. package/server/views/doc.ejs +483 -0
  30. package/server/views/index.ejs +77 -0
  31. package/server/views/not-found.ejs +57 -0
  32. package/server/views/partials/breadcrumbs.ejs +15 -0
  33. package/server/views/partials/diagram-viewer.ejs +16 -0
  34. package/server/views/partials/header.ejs +37 -0
  35. package/server/views/partials/sidebar.ejs +19 -0
@@ -0,0 +1,141 @@
1
+ // 侧边栏功能
2
+ (function() {
3
+ // 等待 DOM 加载完成
4
+ function initSidebarResizer() {
5
+ const sidebar = document.getElementById('sidebar');
6
+ const resizer = document.getElementById('sidebar-resizer');
7
+ const toggle = document.getElementById('sidebar-collapse-toggle');
8
+
9
+ if (!sidebar || !resizer || !toggle) {
10
+ // 如果元素还没加载,延迟重试
11
+ setTimeout(initSidebarResizer, 100);
12
+ return;
13
+ }
14
+
15
+ const COLLAPSE_KEY = 'sidebar-collapsed';
16
+ let isResizing = false;
17
+ let startX = 0;
18
+ let startWidth = 0;
19
+
20
+ function getSidebarBounds() {
21
+ const styles = window.getComputedStyle(sidebar);
22
+ const minWidth = Number.parseFloat(styles.minWidth) || 240;
23
+ const viewportMaxWidth = Math.floor(window.innerWidth * 0.42);
24
+ const cssMaxWidth = Number.parseFloat(styles.maxWidth) || viewportMaxWidth;
25
+ const maxWidth = Math.max(minWidth, Math.min(cssMaxWidth, viewportMaxWidth));
26
+ return { minWidth, maxWidth };
27
+ }
28
+
29
+ function clampWidth(width) {
30
+ const { minWidth, maxWidth } = getSidebarBounds();
31
+ return Math.min(Math.max(width, minWidth), maxWidth);
32
+ }
33
+
34
+ function applySidebarWidth(width, shouldPersist) {
35
+ const nextWidth = clampWidth(width);
36
+ sidebar.style.width = nextWidth + 'px';
37
+ if (shouldPersist) {
38
+ localStorage.setItem('sidebar-width', String(Math.round(nextWidth)));
39
+ }
40
+ }
41
+
42
+ function setCollapsed(collapsed) {
43
+ document.body.classList.toggle('sidebar-collapsed', collapsed);
44
+ toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
45
+ toggle.setAttribute('aria-label', collapsed ? '展开目录' : '收起目录');
46
+ toggle.setAttribute('title', collapsed ? '展开目录' : '收起目录');
47
+ toggle.querySelector('.sidebar-collapse-icon').textContent = collapsed ? '›' : '‹';
48
+ localStorage.setItem(COLLAPSE_KEY, collapsed ? '1' : '0');
49
+ }
50
+
51
+ // 从 localStorage 读取保存的宽度,读不到则使用当前计算宽度兜底
52
+ const savedWidth = Number.parseFloat(localStorage.getItem('sidebar-width') || '');
53
+ const initialWidth = Number.isFinite(savedWidth) ? savedWidth : sidebar.getBoundingClientRect().width;
54
+ applySidebarWidth(initialWidth, false);
55
+ setCollapsed(localStorage.getItem(COLLAPSE_KEY) === '1');
56
+
57
+ resizer.addEventListener('mousedown', function(e) {
58
+ if (document.body.classList.contains('sidebar-collapsed')) return;
59
+ isResizing = true;
60
+ startX = e.clientX;
61
+ startWidth = sidebar.offsetWidth;
62
+ resizer.classList.add('dragging');
63
+ document.body.style.cursor = 'col-resize';
64
+ document.body.style.userSelect = 'none';
65
+ e.preventDefault();
66
+ e.stopPropagation();
67
+ });
68
+
69
+ document.addEventListener('mousemove', function(e) {
70
+ if (!isResizing) return;
71
+
72
+ const diff = e.clientX - startX;
73
+ const newWidth = startWidth + diff;
74
+ applySidebarWidth(newWidth, false);
75
+ e.preventDefault();
76
+ });
77
+
78
+ document.addEventListener('mouseup', function() {
79
+ if (isResizing) {
80
+ isResizing = false;
81
+ resizer.classList.remove('dragging');
82
+ document.body.style.cursor = '';
83
+ document.body.style.userSelect = '';
84
+
85
+ // 保存宽度到 localStorage
86
+ applySidebarWidth(sidebar.offsetWidth, true);
87
+ }
88
+ });
89
+
90
+ toggle.addEventListener('click', function() {
91
+ const willCollapse = !document.body.classList.contains('sidebar-collapsed');
92
+ setCollapsed(willCollapse);
93
+ });
94
+
95
+ window.addEventListener('resize', function() {
96
+ const currentWidth = Number.parseFloat(sidebar.style.width) || sidebar.getBoundingClientRect().width;
97
+ applySidebarWidth(currentWidth, false);
98
+ });
99
+ }
100
+
101
+ // 页面加载完成后初始化
102
+ if (document.readyState === 'loading') {
103
+ document.addEventListener('DOMContentLoaded', initSidebarResizer);
104
+ } else {
105
+ initSidebarResizer();
106
+ }
107
+ })();
108
+
109
+ // 目录折叠/展开功能(图标统一为 ▼,展开=朝下,折叠=CSS 旋转为朝右)
110
+ function toggleDirectory(element) {
111
+ const children = element.nextElementSibling;
112
+ if (children) {
113
+ const directoryItem = element.parentElement;
114
+ const isCollapsed =
115
+ directoryItem.classList.contains('collapsed') ||
116
+ children.style.display === 'none';
117
+ children.style.display = isCollapsed ? 'block' : 'none';
118
+ directoryItem.classList.toggle('collapsed', !isCollapsed);
119
+ if (typeof window.saveTreeOpenState === 'function') window.saveTreeOpenState();
120
+ }
121
+ }
122
+
123
+ // 目录行点击:与普通目录一致,统一为切换展开/折叠(README 需点击文件链接进入)
124
+ // 使用 body 委托,避免依赖 .tree-nav 加载时机
125
+ (function() {
126
+ function onBodyClick(e) {
127
+ const dirToggle = e.target.closest('.tree-nav .dir-toggle');
128
+ if (!dirToggle) return;
129
+ toggleDirectory(dirToggle);
130
+ }
131
+ function bindDirReadmeClick() {
132
+ document.body.removeEventListener('click', onBodyClick);
133
+ document.body.addEventListener('click', onBodyClick);
134
+ }
135
+ if (document.readyState === 'loading') {
136
+ document.addEventListener('DOMContentLoaded', bindDirReadmeClick);
137
+ } else {
138
+ bindDirReadmeClick();
139
+ }
140
+ window.addEventListener('load', bindDirReadmeClick);
141
+ })();
@@ -0,0 +1,138 @@
1
+ // 主题切换功能(支持系统自动跟随)
2
+ (function() {
3
+ // 检测系统主题
4
+ function getSystemTheme() {
5
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
6
+ }
7
+
8
+ // 从 localStorage 读取主题偏好,默认为 'auto'(跟随系统)
9
+ const savedTheme = localStorage.getItem('markdown-theme') || 'auto';
10
+
11
+ // 相对路径,由 resolveThemeCss 统一加上 base 或前导 /
12
+ const themes = {
13
+ light: {
14
+ css: 'vendor/github-markdown-css/github-markdown-light.css',
15
+ icon: '☀️',
16
+ text: '浅色模式'
17
+ },
18
+ dark: {
19
+ css: 'vendor/github-markdown-css/github-markdown-dark.css',
20
+ icon: '🌙',
21
+ text: '深色模式'
22
+ },
23
+ auto: {
24
+ css: '',
25
+ icon: '💻',
26
+ text: '跟随系统'
27
+ }
28
+ };
29
+
30
+ function resolveThemeCss(path) {
31
+ if (!path) return '';
32
+ var base = document.querySelector('base');
33
+ if (base && base.getAttribute('href')) {
34
+ var baseHref = base.getAttribute('href').replace(/\/?$/, '/');
35
+ return baseHref + path.replace(/^\//, '');
36
+ }
37
+ return path.charAt(0) === '/' ? path : '/' + path;
38
+ }
39
+
40
+ // 获取实际要应用的主题(如果是 auto,则使用系统主题)
41
+ function getEffectiveTheme(theme) {
42
+ return theme === 'auto' ? getSystemTheme() : theme;
43
+ }
44
+
45
+ // 三种形态循环顺序:auto → light → dark → auto
46
+ const themeCycle = ['auto', 'light', 'dark'];
47
+
48
+ // 等待 DOM 加载完成
49
+ function initTheme() {
50
+ const themeToggle = document.getElementById('theme-toggle');
51
+ const themeLink = document.getElementById('markdown-theme');
52
+ const themeIcon = themeToggle?.querySelector('.theme-icon');
53
+ const themeText = themeToggle?.querySelector('.theme-text');
54
+
55
+ if (!themeToggle || !themeLink || !themeIcon) {
56
+ if (document.readyState === 'loading') {
57
+ document.addEventListener('DOMContentLoaded', initTheme);
58
+ return;
59
+ }
60
+ console.warn('主题切换元素未找到,可能 DOM 尚未加载完成');
61
+ return;
62
+ }
63
+
64
+ // 更新按钮显示(图标 + 文案)
65
+ function updateButtonDisplay(theme) {
66
+ const themeConfig = themes[theme];
67
+ if (themeIcon) themeIcon.textContent = themeConfig.icon;
68
+ if (themeText) themeText.textContent = themeConfig.text;
69
+ }
70
+
71
+ // 应用主题
72
+ function applyTheme(theme, skipSave) {
73
+ const effectiveTheme = getEffectiveTheme(theme);
74
+ const themeConfig = themes[theme];
75
+ const effectiveConfig = themes[effectiveTheme];
76
+
77
+ themeLink.href = resolveThemeCss(effectiveConfig.css);
78
+ document.body.setAttribute('data-theme', effectiveTheme);
79
+ document.body.setAttribute('data-theme-mode', theme);
80
+
81
+ updateButtonDisplay(theme);
82
+
83
+ if (!skipSave) {
84
+ localStorage.setItem('markdown-theme', theme);
85
+ }
86
+
87
+ if (typeof mermaid !== 'undefined') {
88
+ mermaid.initialize({
89
+ startOnLoad: false,
90
+ theme: effectiveTheme === 'dark' ? 'dark' : 'default',
91
+ securityLevel: 'loose',
92
+ flowchart: {
93
+ useMaxWidth: true,
94
+ htmlLabels: true
95
+ }
96
+ });
97
+ mermaid.run().then(() => {
98
+ if (typeof setupDiagramViewer === 'function') {
99
+ setupDiagramViewer();
100
+ }
101
+ });
102
+ }
103
+ }
104
+
105
+ // 一键循环到下一形态
106
+ function cycleTheme() {
107
+ const current = document.body.getAttribute('data-theme-mode') || savedTheme;
108
+ const idx = themeCycle.indexOf(current);
109
+ const nextIdx = idx >= 0 ? (idx + 1) % themeCycle.length : 0;
110
+ applyTheme(themeCycle[nextIdx]);
111
+ }
112
+
113
+ if (window.matchMedia && !window._themeMediaQueryBound) {
114
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
115
+ mediaQuery.addEventListener('change', function() {
116
+ const currentMode = document.body.getAttribute('data-theme-mode') || savedTheme;
117
+ if (currentMode === 'auto') {
118
+ applyTheme('auto', true);
119
+ }
120
+ });
121
+ window._themeMediaQueryBound = true;
122
+ }
123
+
124
+ applyTheme(savedTheme);
125
+
126
+ themeToggle.addEventListener('click', function(e) {
127
+ e.stopPropagation();
128
+ cycleTheme();
129
+ });
130
+ }
131
+
132
+ // 如果 DOM 已经加载完成,立即执行;否则等待 DOMContentLoaded
133
+ if (document.readyState === 'loading') {
134
+ document.addEventListener('DOMContentLoaded', initTheme);
135
+ } else {
136
+ initTheme();
137
+ }
138
+ })();
@@ -0,0 +1,90 @@
1
+ // 全屏模式下控制栏自动隐藏功能
2
+ let controlsAutoHideTimer = null;
3
+ let controlsMouseMoveTimer = null;
4
+
5
+ function setupControlsAutoHide() {
6
+ const viewer = document.getElementById('diagram-viewer');
7
+ const controls = document.querySelector('.diagram-viewer-controls');
8
+ if (!viewer || !controls) return;
9
+
10
+ // 初始状态:全屏模式下隐藏控制栏
11
+ if (isFullscreen) {
12
+ controls.classList.add('controls-hidden');
13
+ }
14
+
15
+ // 鼠标移动事件处理
16
+ const mouseMoveHandler = function(e) {
17
+ if (!isFullscreen) return;
18
+
19
+ const viewerRect = viewer.getBoundingClientRect();
20
+ const mouseY = e.clientY - viewerRect.top;
21
+ const viewerHeight = viewerRect.height;
22
+ const threshold = 100; // 距离底部100px时显示控制栏
23
+
24
+ // 清除之前的定时器
25
+ if (controlsAutoHideTimer) {
26
+ clearTimeout(controlsAutoHideTimer);
27
+ }
28
+ if (controlsMouseMoveTimer) {
29
+ clearTimeout(controlsMouseMoveTimer);
30
+ }
31
+
32
+ // 如果鼠标在底部区域,显示控制栏
33
+ if (mouseY > viewerHeight - threshold) {
34
+ controls.classList.remove('controls-hidden');
35
+
36
+ // 鼠标静止后隐藏控制栏
37
+ controlsMouseMoveTimer = setTimeout(() => {
38
+ controls.classList.add('controls-hidden');
39
+ }, 2000); // 2秒后隐藏
40
+ } else {
41
+ // 鼠标不在底部区域,立即隐藏
42
+ controls.classList.add('controls-hidden');
43
+ }
44
+ };
45
+
46
+ // 鼠标离开查看器时隐藏控制栏
47
+ const mouseLeaveHandler = function() {
48
+ if (!isFullscreen) return;
49
+ const controls = document.querySelector('.diagram-viewer-controls');
50
+ if (controls) {
51
+ controls.classList.add('controls-hidden');
52
+ }
53
+ };
54
+
55
+ viewer.addEventListener('mousemove', mouseMoveHandler);
56
+ viewer.addEventListener('mouseleave', mouseLeaveHandler);
57
+
58
+ // 保存处理器以便后续移除
59
+ viewer._controlsMouseMoveHandler = mouseMoveHandler;
60
+ viewer._controlsMouseLeaveHandler = mouseLeaveHandler;
61
+ }
62
+
63
+ function removeControlsAutoHide() {
64
+ const viewer = document.getElementById('diagram-viewer');
65
+ if (!viewer) return;
66
+
67
+ if (controlsAutoHideTimer) {
68
+ clearTimeout(controlsAutoHideTimer);
69
+ controlsAutoHideTimer = null;
70
+ }
71
+ if (controlsMouseMoveTimer) {
72
+ clearTimeout(controlsMouseMoveTimer);
73
+ controlsMouseMoveTimer = null;
74
+ }
75
+
76
+ if (viewer._controlsMouseMoveHandler) {
77
+ viewer.removeEventListener('mousemove', viewer._controlsMouseMoveHandler);
78
+ viewer._controlsMouseMoveHandler = null;
79
+ }
80
+ if (viewer._controlsMouseLeaveHandler) {
81
+ viewer.removeEventListener('mouseleave', viewer._controlsMouseLeaveHandler);
82
+ viewer._controlsMouseLeaveHandler = null;
83
+ }
84
+
85
+ // 移除隐藏状态
86
+ const controls = document.querySelector('.diagram-viewer-controls');
87
+ if (controls) {
88
+ controls.classList.remove('controls-hidden');
89
+ }
90
+ }