@mgks/docmd 0.3.4 → 0.3.6
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 +92 -100
- package/bin/docmd.js +55 -93
- package/docmd.config.js +175 -0
- package/package.json +16 -15
- package/scripts/build-live.js +157 -0
- package/scripts/test-live.js +54 -0
- package/src/assets/js/docmd-main.js +35 -106
- package/src/commands/build.js +11 -6
- package/src/commands/dev.js +100 -189
- package/src/commands/init.js +8 -8
- package/src/core/config-loader.js +23 -3
- package/src/core/file-processor.js +8 -2
- package/src/core/html-generator.js +107 -102
- package/src/live/core.js +63 -0
- package/src/live/index.html +201 -0
- package/src/live/live.css +167 -0
- package/src/live/shims.js +1 -0
- package/src/live/templates.js +9 -0
- package/src/templates/layout.ejs +11 -11
- package/src/templates/navigation.ejs +69 -7
- package/config.js +0 -175
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mgks/docmd",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Generate beautiful, lightweight static documentation sites directly from your Markdown files. Zero clutter, just content.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"start": "node ./bin/docmd.js dev",
|
|
17
17
|
"build": "node ./bin/docmd.js build",
|
|
18
|
+
"live": "node scripts/build-live.js && npx serve dist",
|
|
18
19
|
"postinstall": "node ./bin/postinstall.js",
|
|
19
20
|
"lint": "eslint .",
|
|
20
21
|
"format": "prettier --write .",
|
|
@@ -22,32 +23,32 @@
|
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"chalk": "^4.1.2",
|
|
25
|
-
"chokidar": "^
|
|
26
|
+
"chokidar": "^5.0.0",
|
|
26
27
|
"clean-css": "^5.3.3",
|
|
27
|
-
"commander": "^14.0.
|
|
28
|
-
"ejs": "^3.1.
|
|
29
|
-
"esbuild": "^0.27.
|
|
30
|
-
"express": "^5.1
|
|
31
|
-
"fs-extra": "^11.
|
|
28
|
+
"commander": "^14.0.2",
|
|
29
|
+
"ejs": "^3.1.10",
|
|
30
|
+
"esbuild": "^0.27.2",
|
|
31
|
+
"express": "^5.2.1",
|
|
32
|
+
"fs-extra": "^11.3.3",
|
|
32
33
|
"gray-matter": "^4.0.3",
|
|
33
34
|
"highlight.js": "^11.11.1",
|
|
34
|
-
"lucide-static": "^0.
|
|
35
|
+
"lucide-static": "^0.562.0",
|
|
36
|
+
"markdown-it": "^14.1.0",
|
|
35
37
|
"markdown-it-abbr": "^2.0.0",
|
|
36
38
|
"markdown-it-attrs": "^4.3.1",
|
|
37
39
|
"markdown-it-container": "^4.0.0",
|
|
38
40
|
"markdown-it-deflist": "^3.0.0",
|
|
39
41
|
"markdown-it-footnote": "^4.0.0",
|
|
40
42
|
"markdown-it-task-lists": "^2.1.1",
|
|
41
|
-
"mermaid": "^11.12.
|
|
43
|
+
"mermaid": "^11.12.2",
|
|
42
44
|
"minisearch": "^7.2.0",
|
|
43
45
|
"striptags": "^3.2.0",
|
|
44
|
-
"ws": "^8.
|
|
46
|
+
"ws": "^8.18.3"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
|
-
"
|
|
48
|
-
"eslint
|
|
49
|
-
"
|
|
50
|
-
"prettier": "^3.2.5"
|
|
49
|
+
"buffer": "^6.0.3",
|
|
50
|
+
"eslint": "^9.39.2",
|
|
51
|
+
"prettier": "^3.7.4"
|
|
51
52
|
},
|
|
52
53
|
"directories": {
|
|
53
54
|
"doc": "docs"
|
|
@@ -77,4 +78,4 @@
|
|
|
77
78
|
"homepage": "https://github.com/mgks/docmd#readme",
|
|
78
79
|
"funding": "https://github.com/sponsors/mgks",
|
|
79
80
|
"license": "MIT"
|
|
80
|
-
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const esbuild = require('esbuild');
|
|
4
|
+
|
|
5
|
+
async function build() {
|
|
6
|
+
console.log('📦 Building @docmd/live core...');
|
|
7
|
+
|
|
8
|
+
const SRC_DIR = path.join(__dirname, '../src');
|
|
9
|
+
const LIVE_SRC_DIR = path.join(SRC_DIR, 'live');
|
|
10
|
+
const DIST_DIR = path.join(__dirname, '../dist');
|
|
11
|
+
|
|
12
|
+
// Ensure dist exists and is empty
|
|
13
|
+
fs.emptyDirSync(DIST_DIR);
|
|
14
|
+
|
|
15
|
+
// 1. Read Templates
|
|
16
|
+
const templatesDir = path.join(SRC_DIR, 'templates');
|
|
17
|
+
const files = fs.readdirSync(templatesDir);
|
|
18
|
+
const templates = {};
|
|
19
|
+
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
if (file.endsWith('.ejs')) {
|
|
22
|
+
templates[file] = fs.readFileSync(path.join(templatesDir, file), 'utf8');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 2. Generate templates.js
|
|
27
|
+
const templatesJsPath = path.join(LIVE_SRC_DIR, 'templates.js');
|
|
28
|
+
const templatesContent = `
|
|
29
|
+
const templates = ${JSON.stringify(templates, null, 2)};
|
|
30
|
+
if (typeof globalThis !== 'undefined') globalThis.__DOCMD_TEMPLATES__ = templates;
|
|
31
|
+
module.exports = templates;
|
|
32
|
+
`;
|
|
33
|
+
fs.writeFileSync(templatesJsPath, templatesContent);
|
|
34
|
+
|
|
35
|
+
// 3. Generate Shim for Buffer
|
|
36
|
+
const shimPath = path.join(LIVE_SRC_DIR, 'shims.js');
|
|
37
|
+
fs.writeFileSync(shimPath, `import { Buffer } from 'buffer'; globalThis.Buffer = Buffer;`);
|
|
38
|
+
|
|
39
|
+
// 4. Bundle JS
|
|
40
|
+
try {
|
|
41
|
+
await esbuild.build({
|
|
42
|
+
entryPoints: [path.join(LIVE_SRC_DIR, 'core.js')],
|
|
43
|
+
bundle: true,
|
|
44
|
+
outfile: path.join(DIST_DIR, 'docmd-live.js'),
|
|
45
|
+
platform: 'browser',
|
|
46
|
+
format: 'iife',
|
|
47
|
+
globalName: 'docmd',
|
|
48
|
+
minify: true,
|
|
49
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
50
|
+
inject: [shimPath],
|
|
51
|
+
plugins: [
|
|
52
|
+
{
|
|
53
|
+
name: 'node-deps-shim',
|
|
54
|
+
setup(build) {
|
|
55
|
+
build.onResolve({ filter: /^path$/ }, args => ({ path: args.path, namespace: 'path-shim' }));
|
|
56
|
+
build.onLoad({ filter: /.*/, namespace: 'path-shim' }, () => ({
|
|
57
|
+
contents: `
|
|
58
|
+
module.exports = {
|
|
59
|
+
join: (...args) => args.filter(Boolean).join('/'),
|
|
60
|
+
resolve: (...args) => '/' + args.filter(Boolean).join('/'),
|
|
61
|
+
basename: (p) => p ? p.split(/[\\\\/]/).pop() : '',
|
|
62
|
+
dirname: (p) => p ? p.split(/[\\\\/]/).slice(0, -1).join('/') || '.' : '.',
|
|
63
|
+
extname: (p) => {
|
|
64
|
+
if (!p) return '';
|
|
65
|
+
const parts = p.split('.');
|
|
66
|
+
return parts.length > 1 ? '.' + parts.pop() : '';
|
|
67
|
+
},
|
|
68
|
+
isAbsolute: (p) => p.startsWith('/'),
|
|
69
|
+
normalize: (p) => p,
|
|
70
|
+
sep: '/'
|
|
71
|
+
};
|
|
72
|
+
`,
|
|
73
|
+
loader: 'js'
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
build.onResolve({ filter: /^(fs|fs-extra)$/ }, args => ({ path: args.path, namespace: 'fs-shim' }));
|
|
77
|
+
build.onLoad({ filter: /.*/, namespace: 'fs-shim' }, () => ({
|
|
78
|
+
contents: `
|
|
79
|
+
module.exports = {
|
|
80
|
+
existsSync: (p) => {
|
|
81
|
+
if (!globalThis.__DOCMD_TEMPLATES__) return false;
|
|
82
|
+
let name = p.split(/[\\\\/]/).pop();
|
|
83
|
+
if (globalThis.__DOCMD_TEMPLATES__[name]) return true;
|
|
84
|
+
if (!name.endsWith('.ejs') && globalThis.__DOCMD_TEMPLATES__[name + '.ejs']) return true;
|
|
85
|
+
return false;
|
|
86
|
+
},
|
|
87
|
+
readFileSync: (p) => {
|
|
88
|
+
if (!globalThis.__DOCMD_TEMPLATES__) return '';
|
|
89
|
+
let name = p.split(/[\\\\/]/).pop();
|
|
90
|
+
if (globalThis.__DOCMD_TEMPLATES__[name]) return globalThis.__DOCMD_TEMPLATES__[name];
|
|
91
|
+
if (!name.endsWith('.ejs')) name += '.ejs';
|
|
92
|
+
return globalThis.__DOCMD_TEMPLATES__[name] || '';
|
|
93
|
+
},
|
|
94
|
+
statSync: () => ({ isFile: () => true, isDirectory: () => false }),
|
|
95
|
+
constants: { F_OK: 0, R_OK: 4 }
|
|
96
|
+
};
|
|
97
|
+
`,
|
|
98
|
+
loader: 'js'
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
console.log('✅ Bundled JS to dist/docmd-live.js');
|
|
105
|
+
|
|
106
|
+
// 5. Copy Assets
|
|
107
|
+
console.log('📂 Copying assets...');
|
|
108
|
+
await fs.copy(path.join(SRC_DIR, 'assets'), path.join(DIST_DIR, 'assets'));
|
|
109
|
+
|
|
110
|
+
// 5.5 Bundle Third-Party Libraries (The FIX)
|
|
111
|
+
// We need to manually copy these from node_modules because they aren't in src/assets
|
|
112
|
+
const copyLibrary = async (packageName, fileToBundle, destFileName) => {
|
|
113
|
+
try {
|
|
114
|
+
// Try to resolve the package path
|
|
115
|
+
let srcPath;
|
|
116
|
+
try {
|
|
117
|
+
srcPath = require.resolve(`${packageName}/${fileToBundle}`);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
// Fallback search if require.resolve fails
|
|
120
|
+
const mainPath = require.resolve(packageName);
|
|
121
|
+
let currentDir = path.dirname(mainPath);
|
|
122
|
+
for (let i = 0; i < 5; i++) {
|
|
123
|
+
if (fs.existsSync(path.join(currentDir, 'package.json'))) break;
|
|
124
|
+
currentDir = path.dirname(currentDir);
|
|
125
|
+
}
|
|
126
|
+
srcPath = path.join(currentDir, fileToBundle);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (srcPath && fs.existsSync(srcPath)) {
|
|
130
|
+
const destPath = path.join(DIST_DIR, 'assets/js', destFileName);
|
|
131
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
132
|
+
await fs.copy(srcPath, destPath);
|
|
133
|
+
console.log(` └─ Copied ${packageName} -> assets/js/${destFileName}`);
|
|
134
|
+
} else {
|
|
135
|
+
console.warn(`⚠️ Could not locate ${fileToBundle} in ${packageName}`);
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.warn(`⚠️ Failed to bundle ${packageName}: ${e.message}`);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
await copyLibrary('minisearch', 'dist/umd/index.js', 'minisearch.js');
|
|
143
|
+
await copyLibrary('mermaid', 'dist/mermaid.min.js', 'mermaid.min.js');
|
|
144
|
+
console.log('✅ Assets & Libraries copied.');
|
|
145
|
+
|
|
146
|
+
// 6. Copy Demo HTML
|
|
147
|
+
await fs.copy(path.join(LIVE_SRC_DIR, 'index.html'), path.join(DIST_DIR, 'index.html'));
|
|
148
|
+
await fs.copy(path.join(LIVE_SRC_DIR, 'live.css'), path.join(DIST_DIR, 'live.css'));
|
|
149
|
+
console.log('✅ Demo HTML copied.');
|
|
150
|
+
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.error('❌ Build failed:', e);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
build();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const vm = require('vm');
|
|
4
|
+
|
|
5
|
+
const bundlePath = path.join(__dirname, '../dist/docmd-live.js');
|
|
6
|
+
|
|
7
|
+
if (!fs.existsSync(bundlePath)) {
|
|
8
|
+
console.error('❌ Bundle not found. Run "npm run live" or "node scripts/build-live.js" first.');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const bundleCode = fs.readFileSync(bundlePath, 'utf8');
|
|
13
|
+
|
|
14
|
+
const sandbox = {
|
|
15
|
+
console: console,
|
|
16
|
+
setTimeout: setTimeout,
|
|
17
|
+
clearTimeout: clearTimeout,
|
|
18
|
+
window: {},
|
|
19
|
+
self: {},
|
|
20
|
+
globalThis: {}
|
|
21
|
+
};
|
|
22
|
+
sandbox.window = sandbox;
|
|
23
|
+
sandbox.self = sandbox;
|
|
24
|
+
sandbox.globalThis = sandbox;
|
|
25
|
+
|
|
26
|
+
vm.createContext(sandbox);
|
|
27
|
+
|
|
28
|
+
console.log('🧪 Testing Live Bundle...');
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
vm.runInContext(bundleCode, sandbox);
|
|
32
|
+
|
|
33
|
+
if (!sandbox.docmd) {
|
|
34
|
+
throw new Error('docmd global not found in bundle');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const markdown = '# Hello Live\nThis is a test.';
|
|
38
|
+
const config = { siteTitle: 'Live Test' };
|
|
39
|
+
|
|
40
|
+
console.log('Compiling markdown...');
|
|
41
|
+
const result = sandbox.docmd.compile(markdown, config);
|
|
42
|
+
|
|
43
|
+
if (result.includes('<h1>Hello Live</h1>') && result.includes('Live Test')) {
|
|
44
|
+
console.log('✅ Bundle works! Output contains expected HTML.');
|
|
45
|
+
} else {
|
|
46
|
+
console.error('❌ Bundle produced unexpected output.');
|
|
47
|
+
console.log('Output snippet:', result.substring(0, 200));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error('❌ Test failed:', e);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
@@ -9,124 +9,82 @@ function initializeCollapsibleNav() {
|
|
|
9
9
|
const nav = document.querySelector('.sidebar-nav');
|
|
10
10
|
if (!nav) return;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
navStates = JSON.parse(sessionStorage.getItem('docmd-nav-states')) || {};
|
|
16
|
-
} catch (e) { /* silent fail */ }
|
|
12
|
+
// We NO LONGER set initial state here.
|
|
13
|
+
// The HTML arrives with style="display: block" and aria-expanded="true"
|
|
14
|
+
// pre-rendered by the build process. This eliminates the FOUC/Jitter.
|
|
17
15
|
|
|
18
16
|
nav.querySelectorAll('li.collapsible').forEach(item => {
|
|
19
|
-
const navId = item.dataset.navId;
|
|
20
17
|
const anchor = item.querySelector('a');
|
|
21
18
|
const submenu = item.querySelector('.submenu');
|
|
22
19
|
|
|
23
|
-
if (!
|
|
24
|
-
|
|
25
|
-
const isParentActive = item.classList.contains('active-parent');
|
|
26
|
-
// Default to expanded if it's a parent of the active page, otherwise check stored state.
|
|
27
|
-
let isExpanded = isParentActive || (navStates[navId] === true);
|
|
28
|
-
|
|
29
|
-
const toggleSubmenu = (expand) => {
|
|
30
|
-
item.setAttribute('aria-expanded', expand);
|
|
31
|
-
submenu.style.display = expand ? 'block' : 'none';
|
|
32
|
-
navStates[navId] = expand;
|
|
33
|
-
sessionStorage.setItem('docmd-nav-states', JSON.stringify(navStates));
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Set initial state on page load
|
|
37
|
-
toggleSubmenu(isExpanded);
|
|
20
|
+
if (!anchor || !submenu) return;
|
|
38
21
|
|
|
22
|
+
// Only handle CLICK events to toggle state
|
|
39
23
|
anchor.addEventListener('click', (e) => {
|
|
40
|
-
const currentExpanded = item.getAttribute('aria-expanded') === 'true';
|
|
41
24
|
const href = anchor.getAttribute('href');
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!currentExpanded) {
|
|
45
|
-
toggleSubmenu(true);
|
|
46
|
-
} else if (isPlaceholder || e.target.closest('.collapse-icon')) {
|
|
47
|
-
toggleSubmenu(false);
|
|
48
|
-
}
|
|
25
|
+
// If it's a placeholder link (#) OR the user clicked the arrow icon
|
|
26
|
+
const isToggleAction = !href || href === '#' || e.target.closest('.collapse-icon');
|
|
49
27
|
|
|
50
|
-
if (
|
|
28
|
+
if (isToggleAction) {
|
|
51
29
|
e.preventDefault();
|
|
30
|
+
|
|
31
|
+
// Toggle Logic
|
|
32
|
+
const isExpanded = item.getAttribute('aria-expanded') === 'true';
|
|
33
|
+
const newState = !isExpanded;
|
|
34
|
+
|
|
35
|
+
item.setAttribute('aria-expanded', newState);
|
|
36
|
+
submenu.style.display = newState ? 'block' : 'none';
|
|
52
37
|
}
|
|
53
38
|
});
|
|
54
|
-
|
|
55
|
-
/* anchor.addEventListener('click', (e) => {
|
|
56
|
-
// If the click target is the icon, ALWAYS prevent navigation and toggle.
|
|
57
|
-
if (e.target.closest('.collapse-icon')) {
|
|
58
|
-
e.preventDefault();
|
|
59
|
-
toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
|
|
60
|
-
}
|
|
61
|
-
// If the link is just a placeholder, also prevent navigation and toggle.
|
|
62
|
-
else if (anchor.getAttribute('href') === '#') {
|
|
63
|
-
e.preventDefault();
|
|
64
|
-
toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
|
|
65
|
-
}
|
|
66
|
-
// Otherwise, let the click proceed to navigate to the link.
|
|
67
|
-
});*/
|
|
68
39
|
});
|
|
69
40
|
}
|
|
70
41
|
|
|
71
42
|
// --- Mobile Menu Logic ---
|
|
72
43
|
function initializeMobileMenus() {
|
|
73
|
-
// 1. Sidebar Toggle
|
|
74
44
|
const sidebarBtn = document.querySelector('.sidebar-menu-button');
|
|
75
45
|
const sidebar = document.querySelector('.sidebar');
|
|
76
46
|
|
|
77
47
|
if (sidebarBtn && sidebar) {
|
|
78
48
|
sidebarBtn.addEventListener('click', (e) => {
|
|
79
|
-
e.stopPropagation();
|
|
49
|
+
e.stopPropagation();
|
|
80
50
|
sidebar.classList.toggle('mobile-expanded');
|
|
81
51
|
});
|
|
82
52
|
}
|
|
83
53
|
|
|
84
|
-
// 2. TOC Toggle
|
|
85
54
|
const tocBtn = document.querySelector('.toc-menu-button');
|
|
86
55
|
const tocContainer = document.querySelector('.toc-container');
|
|
87
|
-
// Also allow clicking the title text to toggle
|
|
88
56
|
const tocTitle = document.querySelector('.toc-title');
|
|
89
57
|
|
|
90
58
|
const toggleToc = (e) => {
|
|
91
|
-
// Only engage on mobile view (check if button is visible)
|
|
92
59
|
if (window.getComputedStyle(tocBtn).display === 'none') return;
|
|
93
|
-
|
|
94
60
|
e.stopPropagation();
|
|
95
61
|
tocContainer.classList.toggle('mobile-expanded');
|
|
96
62
|
};
|
|
97
63
|
|
|
98
64
|
if (tocBtn && tocContainer) {
|
|
99
65
|
tocBtn.addEventListener('click', toggleToc);
|
|
100
|
-
if (tocTitle)
|
|
101
|
-
tocTitle.addEventListener('click', toggleToc);
|
|
102
|
-
}
|
|
66
|
+
if (tocTitle) tocTitle.addEventListener('click', toggleToc);
|
|
103
67
|
}
|
|
104
68
|
}
|
|
105
69
|
|
|
106
|
-
// --- Sidebar Scroll Preservation ---
|
|
70
|
+
// --- Sidebar Scroll Preservation (Instant Center) ---
|
|
107
71
|
function initializeSidebarScroll() {
|
|
108
72
|
const sidebar = document.querySelector('.sidebar');
|
|
109
73
|
if (!sidebar) return;
|
|
110
74
|
|
|
111
|
-
|
|
112
|
-
|
|
75
|
+
// Wait for the layout to be stable
|
|
76
|
+
requestAnimationFrame(() => {
|
|
77
|
+
// Find the active link
|
|
78
|
+
const activeElement = sidebar.querySelector('a.active');
|
|
113
79
|
|
|
114
80
|
if (activeElement) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (isNotInView) {
|
|
122
|
-
activeElement.scrollIntoView({
|
|
123
|
-
behavior: 'auto',
|
|
124
|
-
block: 'center',
|
|
125
|
-
inline: 'nearest'
|
|
126
|
-
});
|
|
127
|
-
}
|
|
81
|
+
activeElement.scrollIntoView({
|
|
82
|
+
behavior: 'auto', // INSTANT jump (prevents scrolling animation jitter)
|
|
83
|
+
block: 'center', // Center it vertically in the sidebar
|
|
84
|
+
inline: 'nearest'
|
|
85
|
+
});
|
|
128
86
|
}
|
|
129
|
-
}
|
|
87
|
+
});
|
|
130
88
|
}
|
|
131
89
|
|
|
132
90
|
// --- Theme Toggle Logic ---
|
|
@@ -138,7 +96,6 @@ function setupThemeToggleListener() {
|
|
|
138
96
|
document.body.setAttribute('data-theme', theme);
|
|
139
97
|
localStorage.setItem('docmd-theme', theme);
|
|
140
98
|
|
|
141
|
-
// Switch highlight.js theme
|
|
142
99
|
const highlightThemeLink = document.getElementById('highlight-theme');
|
|
143
100
|
if (highlightThemeLink) {
|
|
144
101
|
const newHref = highlightThemeLink.getAttribute('data-base-href') + `docmd-highlight-${theme}.css`;
|
|
@@ -146,7 +103,6 @@ function setupThemeToggleListener() {
|
|
|
146
103
|
}
|
|
147
104
|
}
|
|
148
105
|
|
|
149
|
-
// Add click listener to the toggle button
|
|
150
106
|
if (themeToggleButton) {
|
|
151
107
|
themeToggleButton.addEventListener('click', () => {
|
|
152
108
|
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
@@ -161,22 +117,15 @@ function initializeSidebarToggle() {
|
|
|
161
117
|
const toggleButton = document.getElementById('sidebar-toggle-button');
|
|
162
118
|
const body = document.body;
|
|
163
119
|
|
|
164
|
-
if (!body.classList.contains('sidebar-collapsible') || !toggleButton)
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
120
|
+
if (!body.classList.contains('sidebar-collapsible') || !toggleButton) return;
|
|
167
121
|
|
|
168
122
|
const defaultConfigCollapsed = body.dataset.defaultCollapsed === 'true';
|
|
169
123
|
let isCollapsed = localStorage.getItem('docmd-sidebar-collapsed');
|
|
170
124
|
|
|
171
|
-
if (isCollapsed === null)
|
|
172
|
-
|
|
173
|
-
} else {
|
|
174
|
-
isCollapsed = isCollapsed === 'true';
|
|
175
|
-
}
|
|
125
|
+
if (isCollapsed === null) isCollapsed = defaultConfigCollapsed;
|
|
126
|
+
else isCollapsed = isCollapsed === 'true';
|
|
176
127
|
|
|
177
|
-
if (isCollapsed)
|
|
178
|
-
body.classList.add('sidebar-collapsed');
|
|
179
|
-
}
|
|
128
|
+
if (isCollapsed) body.classList.add('sidebar-collapsed');
|
|
180
129
|
|
|
181
130
|
toggleButton.addEventListener('click', () => {
|
|
182
131
|
body.classList.toggle('sidebar-collapsed');
|
|
@@ -197,9 +146,7 @@ function initializeTabs() {
|
|
|
197
146
|
tabPanes.forEach(pane => pane.classList.remove('active'));
|
|
198
147
|
|
|
199
148
|
navItem.classList.add('active');
|
|
200
|
-
if (tabPanes[index])
|
|
201
|
-
tabPanes[index].classList.add('active');
|
|
202
|
-
}
|
|
149
|
+
if (tabPanes[index]) tabPanes[index].classList.add('active');
|
|
203
150
|
});
|
|
204
151
|
});
|
|
205
152
|
});
|
|
@@ -207,9 +154,7 @@ function initializeTabs() {
|
|
|
207
154
|
|
|
208
155
|
// --- Copy Code Button Logic ---
|
|
209
156
|
function initializeCopyCodeButtons() {
|
|
210
|
-
if (document.body.dataset.copyCodeEnabled !== 'true')
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
157
|
+
if (document.body.dataset.copyCodeEnabled !== 'true') return;
|
|
213
158
|
|
|
214
159
|
const copyIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>`;
|
|
215
160
|
const checkIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
|
|
@@ -218,18 +163,12 @@ function initializeCopyCodeButtons() {
|
|
|
218
163
|
const codeElement = preElement.querySelector('code');
|
|
219
164
|
if (!codeElement) return;
|
|
220
165
|
|
|
221
|
-
// Create a wrapper div around the pre element
|
|
222
166
|
const wrapper = document.createElement('div');
|
|
223
167
|
wrapper.style.position = 'relative';
|
|
224
168
|
wrapper.style.display = 'block';
|
|
225
169
|
|
|
226
|
-
// Insert the wrapper before the pre element
|
|
227
170
|
preElement.parentNode.insertBefore(wrapper, preElement);
|
|
228
|
-
|
|
229
|
-
// Move the pre element into the wrapper
|
|
230
171
|
wrapper.appendChild(preElement);
|
|
231
|
-
|
|
232
|
-
// Remove the relative positioning from pre since wrapper handles it
|
|
233
172
|
preElement.style.position = 'static';
|
|
234
173
|
|
|
235
174
|
const copyButton = document.createElement('button');
|
|
@@ -260,21 +199,11 @@ function syncBodyTheme() {
|
|
|
260
199
|
if (currentTheme && document.body) {
|
|
261
200
|
document.body.setAttribute('data-theme', currentTheme);
|
|
262
201
|
}
|
|
263
|
-
|
|
264
|
-
// Also ensure highlight CSS matches the current theme
|
|
265
|
-
const highlightThemeLink = document.getElementById('highlight-theme');
|
|
266
|
-
if (highlightThemeLink && currentTheme) {
|
|
267
|
-
const baseHref = highlightThemeLink.getAttribute('data-base-href');
|
|
268
|
-
if (baseHref) {
|
|
269
|
-
const newHref = baseHref + `docmd-highlight-${currentTheme}.css`;
|
|
270
|
-
highlightThemeLink.setAttribute('href', newHref);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
202
|
}
|
|
274
203
|
|
|
275
204
|
// --- Main Execution ---
|
|
276
205
|
document.addEventListener('DOMContentLoaded', () => {
|
|
277
|
-
syncBodyTheme();
|
|
206
|
+
syncBodyTheme();
|
|
278
207
|
setupThemeToggleListener();
|
|
279
208
|
initializeSidebarToggle();
|
|
280
209
|
initializeTabs();
|
package/src/commands/build.js
CHANGED
|
@@ -66,6 +66,7 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
66
66
|
const md = createMarkdownItInstance(config);
|
|
67
67
|
const shouldMinify = !options.isDev && config.minify !== false;
|
|
68
68
|
const searchIndexData = [];
|
|
69
|
+
const isOfflineMode = options.offline === true;
|
|
69
70
|
|
|
70
71
|
if (!await fs.pathExists(SRC_DIR)) {
|
|
71
72
|
throw new Error(`Source directory not found: ${formatPathForDisplay(SRC_DIR, CWD)}`);
|
|
@@ -287,10 +288,11 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
287
288
|
const currentPagePathForNav = normalizedPath;
|
|
288
289
|
|
|
289
290
|
const navigationHtml = await generateNavigationHtml(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
291
|
+
config.navigation,
|
|
292
|
+
normalizedPath,
|
|
293
|
+
relativePathToRoot,
|
|
294
|
+
config,
|
|
295
|
+
isOfflineMode
|
|
294
296
|
);
|
|
295
297
|
|
|
296
298
|
// Find previous and next pages for navigation
|
|
@@ -320,9 +322,10 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
320
322
|
nextPage: nextPage,
|
|
321
323
|
currentPagePath: normalizedPath,
|
|
322
324
|
headings: headings || [],
|
|
325
|
+
isOfflineMode,
|
|
323
326
|
};
|
|
324
327
|
|
|
325
|
-
const pageHtml = await generateHtmlPage(pageDataForTemplate);
|
|
328
|
+
const pageHtml = await generateHtmlPage(pageDataForTemplate, isOfflineMode);
|
|
326
329
|
|
|
327
330
|
await fs.ensureDir(path.dirname(finalOutputHtmlPath));
|
|
328
331
|
await fs.writeFile(finalOutputHtmlPath, pageHtml);
|
|
@@ -369,7 +372,9 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
369
372
|
|
|
370
373
|
// Generate search index if enabled
|
|
371
374
|
if (config.search !== false) {
|
|
372
|
-
|
|
375
|
+
if (!options.isDev) {
|
|
376
|
+
console.log('🔍 Generating search index...');
|
|
377
|
+
}
|
|
373
378
|
|
|
374
379
|
// Create MiniSearch instance
|
|
375
380
|
const miniSearch = new MiniSearch({
|