@mgks/docmd 0.1.0 → 0.1.2
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/.github/workflows/publish.yml +1 -1
- package/README.md +2 -2
- package/bin/docmd.js +8 -2
- package/config.js +31 -31
- package/docs/cli-commands.md +13 -0
- package/docs/content/images.md +205 -0
- package/docs/content/index.md +17 -0
- package/docs/theming/assets-management.md +126 -0
- package/docs/theming/custom-css-js.md +3 -6
- package/package.json +2 -1
- package/src/assets/css/{main.css → docmd-main.css} +255 -10
- package/src/assets/css/{theme-sky.css → docmd-theme-sky.css} +153 -1
- package/src/assets/js/docmd-image-lightbox.js +72 -0
- package/src/assets/js/{theme-toggle.js → docmd-theme-toggle.js} +4 -4
- package/src/commands/build.js +151 -9
- package/src/commands/dev.js +103 -17
- package/src/commands/init.js +198 -17
- package/src/core/file-processor.js +40 -0
- package/src/core/html-generator.js +7 -3
- package/src/plugins/sitemap.js +10 -3
- package/src/templates/layout.ejs +5 -63
- package/src/templates/toc.ejs +53 -20
- package/docs/writing-content/index.md +0 -17
- package/src/assets/css/toc.css +0 -76
- /package/docs/{writing-content → content}/custom-containers.md +0 -0
- /package/docs/{writing-content → content}/frontmatter.md +0 -0
- /package/docs/{writing-content → content}/markdown-syntax.md +0 -0
- /package/src/assets/css/{highlight-dark.css → docmd-highlight-dark.css} +0 -0
- /package/src/assets/css/{highlight-light.css → docmd-highlight-light.css} +0 -0
- /package/src/assets/images/{logo-dark.png → docmd-logo-dark.png} +0 -0
- /package/src/assets/images/{logo-light.png → docmd-logo-light.png} +0 -0
package/src/commands/build.js
CHANGED
|
@@ -6,6 +6,7 @@ const { processMarkdownFile } = require('../core/file-processor');
|
|
|
6
6
|
const { generateHtmlPage, generateNavigationHtml } = require('../core/html-generator');
|
|
7
7
|
const { renderIcon, clearWarnedIcons } = require('../core/icon-renderer'); // Update import
|
|
8
8
|
const { generateSitemap } = require('../plugins/sitemap'); // Import our sitemap plugin
|
|
9
|
+
const { version } = require('../../package.json'); // Import package version
|
|
9
10
|
|
|
10
11
|
// Debug function to log navigation information
|
|
11
12
|
function logNavigationPaths(pagePath, navPath, normalizedPath) {
|
|
@@ -17,37 +18,119 @@ function logNavigationPaths(pagePath, navPath, normalizedPath) {
|
|
|
17
18
|
// Add a global or scoped flag to track if the warning has been shown in the current dev session
|
|
18
19
|
let highlightWarningShown = false;
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
// Asset version metadata - update this when making significant changes to assets
|
|
22
|
+
const ASSET_VERSIONS = {
|
|
23
|
+
'css/docmd-main.css': { version: version, description: 'Core styles' },
|
|
24
|
+
'css/docmd-theme-sky.css': { version: version, description: 'Sky theme' },
|
|
25
|
+
'css/docmd-highlight-light.css': { version: version, description: 'Light syntax highlighting' },
|
|
26
|
+
'css/docmd-highlight-dark.css': { version: version, description: 'Dark syntax highlighting' },
|
|
27
|
+
'js/docmd-theme-toggle.js': { version: version, description: 'Theme toggle functionality' },
|
|
28
|
+
// Add other assets here with their versions
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
async function buildSite(configPath, options = { isDev: false, preserve: false }) {
|
|
21
32
|
clearWarnedIcons(); // Clear warnings at the start of every build
|
|
22
33
|
|
|
23
34
|
const config = await loadConfig(configPath);
|
|
24
35
|
const CWD = process.cwd();
|
|
25
36
|
const SRC_DIR = path.resolve(CWD, config.srcDir);
|
|
26
37
|
const OUTPUT_DIR = path.resolve(CWD, config.outputDir);
|
|
38
|
+
const USER_ASSETS_DIR = path.resolve(CWD, 'assets'); // User's custom assets directory
|
|
27
39
|
|
|
28
40
|
if (!await fs.pathExists(SRC_DIR)) {
|
|
29
41
|
throw new Error(`Source directory not found: ${SRC_DIR}`);
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
// Create output directory if it doesn't exist
|
|
45
|
+
await fs.ensureDir(OUTPUT_DIR);
|
|
46
|
+
|
|
47
|
+
// Instead of emptying the entire directory, we'll selectively clean up HTML files
|
|
48
|
+
// This preserves custom assets while ensuring we don't have stale HTML files
|
|
49
|
+
if (await fs.pathExists(OUTPUT_DIR)) {
|
|
50
|
+
const cleanupFiles = await findFilesToCleanup(OUTPUT_DIR);
|
|
51
|
+
for (const file of cleanupFiles) {
|
|
52
|
+
await fs.remove(file);
|
|
53
|
+
}
|
|
54
|
+
if (!options.isDev) {
|
|
55
|
+
console.log(`🧹 Cleaned HTML files from output directory: ${OUTPUT_DIR}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Track preserved files for summary report
|
|
60
|
+
const preservedFiles = [];
|
|
61
|
+
const userAssetsCopied = [];
|
|
62
|
+
|
|
63
|
+
// Copy user assets from root assets/ directory if it exists
|
|
64
|
+
if (await fs.pathExists(USER_ASSETS_DIR)) {
|
|
65
|
+
const assetsDestDir = path.join(OUTPUT_DIR, 'assets');
|
|
66
|
+
await fs.ensureDir(assetsDestDir);
|
|
67
|
+
|
|
68
|
+
if (!options.isDev) {
|
|
69
|
+
console.log(`📂 Copying user assets from ${USER_ASSETS_DIR} to ${assetsDestDir}...`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const userAssetFiles = await getAllFiles(USER_ASSETS_DIR);
|
|
73
|
+
|
|
74
|
+
for (const srcFile of userAssetFiles) {
|
|
75
|
+
const relativePath = path.relative(USER_ASSETS_DIR, srcFile);
|
|
76
|
+
const destFile = path.join(assetsDestDir, relativePath);
|
|
77
|
+
|
|
78
|
+
// Ensure directory exists
|
|
79
|
+
await fs.ensureDir(path.dirname(destFile));
|
|
80
|
+
await fs.copyFile(srcFile, destFile);
|
|
81
|
+
userAssetsCopied.push(relativePath);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!options.isDev && userAssetsCopied.length > 0) {
|
|
85
|
+
console.log(`📦 Copied ${userAssetsCopied.length} user assets`);
|
|
86
|
+
}
|
|
35
87
|
}
|
|
36
88
|
|
|
89
|
+
// Copy assets
|
|
37
90
|
const assetsSrcDir = path.join(__dirname, '..', 'assets');
|
|
38
91
|
const assetsDestDir = path.join(OUTPUT_DIR, 'assets');
|
|
92
|
+
|
|
39
93
|
if (await fs.pathExists(assetsSrcDir)) {
|
|
40
|
-
await fs.copy(assetsSrcDir, assetsDestDir);
|
|
41
94
|
if (!options.isDev) {
|
|
42
|
-
|
|
95
|
+
console.log(`📂 Copying docmd assets to ${assetsDestDir}...`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Create destination directory if it doesn't exist
|
|
99
|
+
await fs.ensureDir(assetsDestDir);
|
|
100
|
+
|
|
101
|
+
// Get all files from source directory recursively
|
|
102
|
+
const assetFiles = await getAllFiles(assetsSrcDir);
|
|
103
|
+
|
|
104
|
+
// Copy each file individually, checking for existing files if preserve flag is set
|
|
105
|
+
for (const srcFile of assetFiles) {
|
|
106
|
+
const relativePath = path.relative(assetsSrcDir, srcFile);
|
|
107
|
+
const destFile = path.join(assetsDestDir, relativePath);
|
|
108
|
+
|
|
109
|
+
// Check if destination file already exists
|
|
110
|
+
const fileExists = await fs.pathExists(destFile);
|
|
111
|
+
|
|
112
|
+
// Skip if the file exists and either:
|
|
113
|
+
// 1. The preserve flag is set, OR
|
|
114
|
+
// 2. The file was copied from user assets (user assets take precedence)
|
|
115
|
+
if (fileExists && (options.preserve || userAssetsCopied.includes(relativePath))) {
|
|
116
|
+
// Skip file and add to preserved list
|
|
117
|
+
preservedFiles.push(relativePath);
|
|
118
|
+
if (!options.isDev && options.preserve) {
|
|
119
|
+
console.log(` Preserving existing file: ${relativePath}`);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
// Copy file (either it doesn't exist or we're not preserving)
|
|
123
|
+
await fs.ensureDir(path.dirname(destFile));
|
|
124
|
+
await fs.copyFile(srcFile, destFile);
|
|
125
|
+
}
|
|
43
126
|
}
|
|
44
127
|
} else {
|
|
45
128
|
console.warn(`⚠️ Assets source directory not found: ${assetsSrcDir}`);
|
|
46
129
|
}
|
|
47
130
|
|
|
48
131
|
// Check for Highlight.js themes
|
|
49
|
-
const lightThemePath = path.join(__dirname, '..', 'assets', 'css', 'highlight-light.css');
|
|
50
|
-
const darkThemePath = path.join(__dirname, '..', 'assets', 'css', 'highlight-dark.css');
|
|
132
|
+
const lightThemePath = path.join(__dirname, '..', 'assets', 'css', 'docmd-highlight-light.css');
|
|
133
|
+
const darkThemePath = path.join(__dirname, '..', 'assets', 'css', 'docmd-highlight-dark.css');
|
|
51
134
|
|
|
52
135
|
const themesMissing = !await fs.pathExists(lightThemePath) || !await fs.pathExists(darkThemePath);
|
|
53
136
|
|
|
@@ -275,11 +358,70 @@ async function buildSite(configPath, options = { isDev: false }) {
|
|
|
275
358
|
// Generate sitemap if enabled in config
|
|
276
359
|
if (config.plugins?.sitemap !== false) {
|
|
277
360
|
try {
|
|
278
|
-
await generateSitemap(config, processedPages, OUTPUT_DIR);
|
|
361
|
+
await generateSitemap(config, processedPages, OUTPUT_DIR, { isDev: options.isDev });
|
|
279
362
|
} catch (error) {
|
|
280
363
|
console.error(`❌ Error generating sitemap: ${error.message}`);
|
|
281
364
|
}
|
|
282
365
|
}
|
|
366
|
+
|
|
367
|
+
// Print summary of preserved files at the end of build
|
|
368
|
+
if (preservedFiles.length > 0 && !options.isDev) {
|
|
369
|
+
console.log(`\n📋 Build Summary: ${preservedFiles.length} existing files were preserved:`);
|
|
370
|
+
preservedFiles.forEach(file => console.log(` - assets/${file}`));
|
|
371
|
+
console.log(`\nTo update these files in future builds, run without the --preserve flag.`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (userAssetsCopied.length > 0 && !options.isDev) {
|
|
375
|
+
console.log(`\n📋 User Assets: ${userAssetsCopied.length} files were copied from your assets/ directory:`);
|
|
376
|
+
if (userAssetsCopied.length <= 10) {
|
|
377
|
+
userAssetsCopied.forEach(file => console.log(` - assets/${file}`));
|
|
378
|
+
} else {
|
|
379
|
+
userAssetsCopied.slice(0, 5).forEach(file => console.log(` - assets/${file}`));
|
|
380
|
+
console.log(` - ... and ${userAssetsCopied.length - 5} more files`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Helper function to find HTML files and sitemap.xml to clean up
|
|
386
|
+
async function findFilesToCleanup(dir) {
|
|
387
|
+
const filesToRemove = [];
|
|
388
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
389
|
+
|
|
390
|
+
for (const item of items) {
|
|
391
|
+
const fullPath = path.join(dir, item.name);
|
|
392
|
+
|
|
393
|
+
if (item.isDirectory()) {
|
|
394
|
+
// Don't delete the assets directory
|
|
395
|
+
if (item.name !== 'assets') {
|
|
396
|
+
const subDirFiles = await findFilesToCleanup(fullPath);
|
|
397
|
+
filesToRemove.push(...subDirFiles);
|
|
398
|
+
}
|
|
399
|
+
} else if (
|
|
400
|
+
item.name.endsWith('.html') ||
|
|
401
|
+
item.name === 'sitemap.xml'
|
|
402
|
+
) {
|
|
403
|
+
filesToRemove.push(fullPath);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return filesToRemove;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Helper function to recursively get all files in a directory
|
|
411
|
+
async function getAllFiles(dir) {
|
|
412
|
+
const files = [];
|
|
413
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
414
|
+
|
|
415
|
+
for (const item of items) {
|
|
416
|
+
const fullPath = path.join(dir, item.name);
|
|
417
|
+
if (item.isDirectory()) {
|
|
418
|
+
files.push(...await getAllFiles(fullPath));
|
|
419
|
+
} else {
|
|
420
|
+
files.push(fullPath);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return files;
|
|
283
425
|
}
|
|
284
426
|
|
|
285
427
|
// findMarkdownFiles function remains the same
|
package/src/commands/dev.js
CHANGED
|
@@ -8,7 +8,7 @@ const fs = require('fs-extra');
|
|
|
8
8
|
const { buildSite } = require('./build'); // Re-use the build logic
|
|
9
9
|
const { loadConfig } = require('../core/config-loader');
|
|
10
10
|
|
|
11
|
-
async function startDevServer(configPathOption) {
|
|
11
|
+
async function startDevServer(configPathOption, options = { preserve: false }) {
|
|
12
12
|
let config = await loadConfig(configPathOption); // Load initial config
|
|
13
13
|
const CWD = process.cwd(); // Current Working Directory where user runs `docmd dev`
|
|
14
14
|
|
|
@@ -18,6 +18,7 @@ async function startDevServer(configPathOption) {
|
|
|
18
18
|
outputDir: path.resolve(CWD, currentConfig.outputDir),
|
|
19
19
|
srcDirToWatch: path.resolve(CWD, currentConfig.srcDir),
|
|
20
20
|
configFileToWatch: path.resolve(CWD, configPathOption), // Path to the config file itself
|
|
21
|
+
userAssetsDir: path.resolve(CWD, 'assets'), // User's assets directory
|
|
21
22
|
};
|
|
22
23
|
};
|
|
23
24
|
|
|
@@ -65,11 +66,72 @@ async function startDevServer(configPathOption) {
|
|
|
65
66
|
if (typeof body === 'string') {
|
|
66
67
|
const liveReloadScript = `
|
|
67
68
|
<script>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
(function() {
|
|
70
|
+
// More robust WebSocket connection with automatic reconnection
|
|
71
|
+
let socket;
|
|
72
|
+
let reconnectAttempts = 0;
|
|
73
|
+
const maxReconnectAttempts = 5;
|
|
74
|
+
const reconnectDelay = 1000; // Start with 1 second delay
|
|
75
|
+
|
|
76
|
+
function connect() {
|
|
77
|
+
socket = new WebSocket(\`ws://\${window.location.host}\`);
|
|
78
|
+
|
|
79
|
+
socket.onmessage = function(event) {
|
|
80
|
+
if (event.data === 'reload') {
|
|
81
|
+
console.log('Received reload signal. Refreshing page...');
|
|
82
|
+
window.location.reload();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
socket.onopen = function() {
|
|
87
|
+
console.log('Live reload connected.');
|
|
88
|
+
reconnectAttempts = 0; // Reset reconnect counter on successful connection
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
socket.onclose = function() {
|
|
92
|
+
if (reconnectAttempts < maxReconnectAttempts) {
|
|
93
|
+
reconnectAttempts++;
|
|
94
|
+
const delay = reconnectDelay * Math.pow(1.5, reconnectAttempts - 1); // Exponential backoff
|
|
95
|
+
console.log(\`Live reload disconnected. Reconnecting in \${delay/1000} seconds...\`);
|
|
96
|
+
setTimeout(connect, delay);
|
|
97
|
+
} else {
|
|
98
|
+
console.log('Live reload disconnected. Max reconnect attempts reached.');
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
socket.onerror = function(error) {
|
|
103
|
+
console.error('WebSocket error:', error);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Initial connection
|
|
108
|
+
connect();
|
|
109
|
+
|
|
110
|
+
// Backup reload mechanism using polling for browsers with WebSocket issues
|
|
111
|
+
let lastModified = new Date().getTime();
|
|
112
|
+
const pollInterval = 2000; // Poll every 2 seconds
|
|
113
|
+
|
|
114
|
+
function checkForChanges() {
|
|
115
|
+
fetch(window.location.href, { method: 'HEAD', cache: 'no-store' })
|
|
116
|
+
.then(response => {
|
|
117
|
+
const serverLastModified = new Date(response.headers.get('Last-Modified')).getTime();
|
|
118
|
+
if (serverLastModified > lastModified) {
|
|
119
|
+
console.log('Change detected via polling. Refreshing page...');
|
|
120
|
+
window.location.reload();
|
|
121
|
+
}
|
|
122
|
+
lastModified = serverLastModified;
|
|
123
|
+
})
|
|
124
|
+
.catch(error => console.error('Error checking for changes:', error));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Only use polling as a fallback if WebSocket fails
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
130
|
+
console.log('WebSocket not connected. Falling back to polling.');
|
|
131
|
+
setInterval(checkForChanges, pollInterval);
|
|
132
|
+
}
|
|
133
|
+
}, 5000);
|
|
134
|
+
})();
|
|
73
135
|
</script>
|
|
74
136
|
`;
|
|
75
137
|
body = body.replace('</body>', `${liveReloadScript}</body>`);
|
|
@@ -80,6 +142,12 @@ async function startDevServer(configPathOption) {
|
|
|
80
142
|
next();
|
|
81
143
|
});
|
|
82
144
|
|
|
145
|
+
// Add Last-Modified header to all responses for polling fallback
|
|
146
|
+
app.use((req, res, next) => {
|
|
147
|
+
res.setHeader('Last-Modified', new Date().toUTCString());
|
|
148
|
+
next();
|
|
149
|
+
});
|
|
150
|
+
|
|
83
151
|
// Serve static files from the output directory
|
|
84
152
|
// This middleware needs to be dynamic if outputDir changes
|
|
85
153
|
let staticMiddleware = express.static(paths.outputDir);
|
|
@@ -88,28 +156,45 @@ async function startDevServer(configPathOption) {
|
|
|
88
156
|
// Initial build
|
|
89
157
|
console.log('🚀 Performing initial build for dev server...');
|
|
90
158
|
try {
|
|
91
|
-
await buildSite(configPathOption, { isDev: true }); // Use the original config path option
|
|
159
|
+
await buildSite(configPathOption, { isDev: true, preserve: options.preserve }); // Use the original config path option
|
|
92
160
|
console.log('✅ Initial build complete.');
|
|
93
161
|
} catch (error) {
|
|
94
162
|
console.error('❌ Initial build failed:', error.message, error.stack);
|
|
95
163
|
// Optionally, don't start server if initial build fails, or serve a specific error page.
|
|
96
164
|
}
|
|
97
165
|
|
|
166
|
+
// Check if user assets directory exists
|
|
167
|
+
const userAssetsDirExists = await fs.pathExists(paths.userAssetsDir);
|
|
98
168
|
|
|
99
169
|
// Watch for changes
|
|
100
170
|
const watchedPaths = [
|
|
101
171
|
paths.srcDirToWatch,
|
|
102
172
|
paths.configFileToWatch,
|
|
103
|
-
DOCMD_TEMPLATES_DIR,
|
|
104
|
-
DOCMD_ASSETS_DIR
|
|
105
173
|
];
|
|
106
174
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
175
|
+
// Add user assets directory to watched paths if it exists
|
|
176
|
+
if (userAssetsDirExists) {
|
|
177
|
+
watchedPaths.push(paths.userAssetsDir);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Add internal paths for docmd development (not shown to end users)
|
|
181
|
+
const internalPaths = [DOCMD_TEMPLATES_DIR, DOCMD_ASSETS_DIR];
|
|
182
|
+
|
|
183
|
+
// Only in development environments, we might want to watch internal files too
|
|
184
|
+
if (process.env.DOCMD_DEV === 'true') {
|
|
185
|
+
watchedPaths.push(...internalPaths);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(`👀 Watching for changes in:`);
|
|
189
|
+
console.log(` - Source: ${paths.srcDirToWatch}`);
|
|
190
|
+
console.log(` - Config: ${paths.configFileToWatch}`);
|
|
191
|
+
if (userAssetsDirExists) {
|
|
192
|
+
console.log(` - Assets: ${paths.userAssetsDir}`);
|
|
193
|
+
}
|
|
194
|
+
if (process.env.DOCMD_DEV === 'true') {
|
|
195
|
+
console.log(` - docmd Templates: ${DOCMD_TEMPLATES_DIR} (internal)`);
|
|
196
|
+
console.log(` - docmd Assets: ${DOCMD_ASSETS_DIR} (internal)`);
|
|
197
|
+
}
|
|
113
198
|
|
|
114
199
|
const watcher = chokidar.watch(watchedPaths, {
|
|
115
200
|
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
|
@@ -143,9 +228,9 @@ async function startDevServer(configPathOption) {
|
|
|
143
228
|
paths = newPaths; // Update paths for next build reference
|
|
144
229
|
}
|
|
145
230
|
|
|
146
|
-
await buildSite(configPathOption, { isDev: true }); // Re-build using the potentially updated config path
|
|
231
|
+
await buildSite(configPathOption, { isDev: true, preserve: options.preserve }); // Re-build using the potentially updated config path
|
|
147
232
|
broadcastReload();
|
|
148
|
-
console.log('✅ Rebuild complete. Browser
|
|
233
|
+
console.log('✅ Rebuild complete. Browser will refresh automatically.');
|
|
149
234
|
} catch (error) {
|
|
150
235
|
console.error('❌ Rebuild failed:', error.message, error.stack);
|
|
151
236
|
}
|
|
@@ -164,6 +249,7 @@ async function startDevServer(configPathOption) {
|
|
|
164
249
|
}
|
|
165
250
|
console.log(`🎉 Dev server started at http://localhost:${PORT}`);
|
|
166
251
|
console.log(`Serving content from: ${paths.outputDir}`);
|
|
252
|
+
console.log(`Live reload is active. Browser will refresh automatically when files change.`);
|
|
167
253
|
});
|
|
168
254
|
|
|
169
255
|
// Graceful shutdown
|
package/src/commands/init.js
CHANGED
|
@@ -1,23 +1,103 @@
|
|
|
1
1
|
const fs = require('fs-extra');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const readline = require('readline');
|
|
3
4
|
|
|
4
|
-
const defaultConfigContent = `// config.js
|
|
5
|
+
const defaultConfigContent = `// config.js: basic config for docmd
|
|
5
6
|
module.exports = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// Core Site Metadata
|
|
8
|
+
siteTitle: 'docmd',
|
|
9
|
+
// Define a base URL for your site, crucial for SEO and absolute paths
|
|
10
|
+
// No trailing slash
|
|
11
|
+
siteUrl: '', // Replace with your actual deployed URL
|
|
12
|
+
|
|
13
|
+
// Logo Configuration
|
|
14
|
+
logo: {
|
|
15
|
+
light: '/assets/images/docmd-logo-light.png', // Path relative to outputDir root
|
|
16
|
+
dark: '/assets/images/docmd-logo-dark.png', // Path relative to outputDir root
|
|
17
|
+
alt: 'docmd logo', // Alt text for the logo
|
|
18
|
+
href: '/', // Link for the logo, defaults to site root
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// Directory Configuration
|
|
22
|
+
srcDir: 'docs', // Source directory for Markdown files
|
|
23
|
+
outputDir: 'site', // Directory for generated static site
|
|
24
|
+
|
|
25
|
+
// Theme Configuration
|
|
9
26
|
theme: {
|
|
10
|
-
|
|
27
|
+
name: 'sky', // Themes: 'default', 'sky'
|
|
28
|
+
defaultMode: 'light', // Initial color mode: 'light' or 'dark'
|
|
29
|
+
enableModeToggle: true, // Show UI button to toggle light/dark modes
|
|
30
|
+
customCss: [ // Array of paths to custom CSS files
|
|
31
|
+
// '/assets/css/custom.css', // Custom TOC styles
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Custom JavaScript Files
|
|
36
|
+
customJs: [ // Array of paths to custom JS files, loaded at end of body
|
|
37
|
+
// '/assets/js/custom-script.js', // Paths relative to outputDir root
|
|
38
|
+
],
|
|
39
|
+
|
|
40
|
+
// Plugins Configuration
|
|
41
|
+
// Plugins are configured here. docmd will look for these keys.
|
|
42
|
+
plugins: {
|
|
43
|
+
// SEO Plugin Configuration
|
|
44
|
+
// Most SEO data is pulled from page frontmatter (title, description, image, etc.)
|
|
45
|
+
// These are fallbacks or site-wide settings.
|
|
46
|
+
seo: {
|
|
47
|
+
// Default meta description if a page doesn't have one in its frontmatter
|
|
48
|
+
defaultDescription: 'docmd is a Node.js command-line tool for generating beautiful, lightweight static documentation sites from Markdown files.',
|
|
49
|
+
openGraph: { // For Facebook, LinkedIn, etc.
|
|
50
|
+
// siteName: 'docmd Documentation', // Optional, defaults to config.siteTitle
|
|
51
|
+
// Default image for og:image if not specified in page frontmatter
|
|
52
|
+
// Path relative to outputDir root
|
|
53
|
+
defaultImage: '/assets/images/docmd-preview.png',
|
|
54
|
+
},
|
|
55
|
+
twitter: { // For Twitter Cards
|
|
56
|
+
cardType: 'summary_large_image', // 'summary', 'summary_large_image'
|
|
57
|
+
// siteUsername: '@docmd_handle', // Your site's Twitter handle (optional)
|
|
58
|
+
// creatorUsername: '@your_handle', // Default author handle (optional, can be overridden in frontmatter)
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
// Analytics Plugin Configuration
|
|
62
|
+
analytics: {
|
|
63
|
+
// Google Analytics 4 (GA4)
|
|
64
|
+
googleV4: {
|
|
65
|
+
measurementId: 'G-8QVBDQ4KM1' // Replace with your actual GA4 Measurement ID
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
// Enable Sitemap plugin
|
|
69
|
+
sitemap: {
|
|
70
|
+
defaultChangefreq: 'weekly',
|
|
71
|
+
defaultPriority: 0.8
|
|
72
|
+
}
|
|
73
|
+
// Add other future plugin configurations here by their key
|
|
11
74
|
},
|
|
75
|
+
|
|
76
|
+
// Navigation Structure (Sidebar)
|
|
77
|
+
// Icons are kebab-case names from Lucide Icons (https://lucide.dev/)
|
|
12
78
|
navigation: [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
79
|
+
{ title: 'Welcome', path: '/', icon: 'home' }, // Corresponds to docs/index.md
|
|
80
|
+
{
|
|
81
|
+
title: 'Getting Started',
|
|
82
|
+
icon: 'rocket',
|
|
83
|
+
path: '#',
|
|
84
|
+
children: [
|
|
85
|
+
{ title: 'Documentation', path: 'https://docmd.mgks.dev', icon: 'scroll', external: true },
|
|
86
|
+
{ title: 'Installation', path: 'https://docmd.mgks.dev/getting-started/installation', icon: 'download', external: true },
|
|
87
|
+
{ title: 'Basic Usage', path: 'https://docmd.mgks.dev/getting-started/basic-usage', icon: 'play', external: true },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
// External links:
|
|
91
|
+
{ title: 'GitHub', path: 'https://github.com/mgks/docmd', icon: 'github', external: true },
|
|
20
92
|
],
|
|
93
|
+
|
|
94
|
+
// Footer Configuration
|
|
95
|
+
// Markdown is supported here.
|
|
96
|
+
footer: '© ' + new Date().getFullYear() + ' Project.',
|
|
97
|
+
|
|
98
|
+
// Favicon Configuration
|
|
99
|
+
// Path relative to outputDir root
|
|
100
|
+
favicon: '/assets/favicon.ico',
|
|
21
101
|
};
|
|
22
102
|
`;
|
|
23
103
|
|
|
@@ -36,16 +116,117 @@ async function initProject() {
|
|
|
36
116
|
const docsDir = path.join(baseDir, 'docs');
|
|
37
117
|
const configFile = path.join(baseDir, 'config.js');
|
|
38
118
|
const indexMdFile = path.join(docsDir, 'index.md');
|
|
119
|
+
const assetsDir = path.join(baseDir, 'assets');
|
|
120
|
+
const assetsCssDir = path.join(assetsDir, 'css');
|
|
121
|
+
const assetsJsDir = path.join(assetsDir, 'js');
|
|
122
|
+
const assetsImagesDir = path.join(assetsDir, 'images');
|
|
123
|
+
|
|
124
|
+
const existingFiles = [];
|
|
125
|
+
const dirExists = {
|
|
126
|
+
docs: false,
|
|
127
|
+
assets: false
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Check each file individually
|
|
131
|
+
if (await fs.pathExists(configFile)) {
|
|
132
|
+
existingFiles.push('config.js');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (await fs.pathExists(docsDir)) {
|
|
136
|
+
dirExists.docs = true;
|
|
137
|
+
|
|
138
|
+
if (await fs.pathExists(indexMdFile)) {
|
|
139
|
+
existingFiles.push('docs/index.md');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
39
142
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
143
|
+
// Check if assets directory exists
|
|
144
|
+
if (await fs.pathExists(assetsDir)) {
|
|
145
|
+
dirExists.assets = true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Determine if we should override existing files
|
|
149
|
+
let shouldOverride = false;
|
|
150
|
+
if (existingFiles.length > 0) {
|
|
151
|
+
console.warn('⚠️ The following files already exist:');
|
|
152
|
+
existingFiles.forEach(file => console.warn(` - ${file}`));
|
|
153
|
+
|
|
154
|
+
const rl = readline.createInterface({
|
|
155
|
+
input: process.stdin,
|
|
156
|
+
output: process.stdout
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const answer = await new Promise(resolve => {
|
|
160
|
+
rl.question('Do you want to override these files? (y/N): ', resolve);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
rl.close();
|
|
164
|
+
|
|
165
|
+
shouldOverride = answer.toLowerCase() === 'y';
|
|
166
|
+
|
|
167
|
+
if (!shouldOverride) {
|
|
168
|
+
console.log('⏭️ Skipping existing files. Will only create new files.');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Create docs directory if it doesn't exist
|
|
173
|
+
if (!dirExists.docs) {
|
|
43
174
|
await fs.ensureDir(docsDir);
|
|
175
|
+
console.log('📁 Created `docs/` directory');
|
|
176
|
+
} else {
|
|
177
|
+
console.log('📁 Using existing `docs/` directory');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Create assets directory structure if it doesn't exist
|
|
181
|
+
if (!dirExists.assets) {
|
|
182
|
+
await fs.ensureDir(assetsDir);
|
|
183
|
+
await fs.ensureDir(assetsCssDir);
|
|
184
|
+
await fs.ensureDir(assetsJsDir);
|
|
185
|
+
await fs.ensureDir(assetsImagesDir);
|
|
186
|
+
console.log('📁 Created `assets/` directory with css, js, and images subdirectories');
|
|
187
|
+
} else {
|
|
188
|
+
console.log('📁 Using existing `assets/` directory');
|
|
189
|
+
|
|
190
|
+
// Create subdirectories if they don't exist
|
|
191
|
+
if (!await fs.pathExists(assetsCssDir)) {
|
|
192
|
+
await fs.ensureDir(assetsCssDir);
|
|
193
|
+
console.log('📁 Created `assets/css/` directory');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!await fs.pathExists(assetsJsDir)) {
|
|
197
|
+
await fs.ensureDir(assetsJsDir);
|
|
198
|
+
console.log('📁 Created `assets/js/` directory');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!await fs.pathExists(assetsImagesDir)) {
|
|
202
|
+
await fs.ensureDir(assetsImagesDir);
|
|
203
|
+
console.log('📁 Created `assets/images/` directory');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Write config file if it doesn't exist or user confirmed override
|
|
208
|
+
if (!await fs.pathExists(configFile)) {
|
|
44
209
|
await fs.writeFile(configFile, defaultConfigContent, 'utf8');
|
|
45
|
-
await fs.writeFile(indexMdFile, defaultIndexMdContent, 'utf8');
|
|
46
210
|
console.log('📄 Created `config.js`');
|
|
47
|
-
|
|
211
|
+
} else if (shouldOverride) {
|
|
212
|
+
await fs.writeFile(configFile, defaultConfigContent, 'utf8');
|
|
213
|
+
console.log('📄 Updated `config.js`');
|
|
214
|
+
} else {
|
|
215
|
+
console.log('⏭️ Skipped existing `config.js`');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Write index.md file if it doesn't exist or user confirmed override
|
|
219
|
+
if (!await fs.pathExists(indexMdFile)) {
|
|
220
|
+
await fs.writeFile(indexMdFile, defaultIndexMdContent, 'utf8');
|
|
221
|
+
console.log('📄 Created `docs/index.md`');
|
|
222
|
+
} else if (shouldOverride) {
|
|
223
|
+
await fs.writeFile(indexMdFile, defaultIndexMdContent, 'utf8');
|
|
224
|
+
console.log('📄 Updated `docs/index.md`');
|
|
225
|
+
} else {
|
|
226
|
+
console.log('⏭️ Skipped existing `docs/index.md`');
|
|
48
227
|
}
|
|
228
|
+
|
|
229
|
+
console.log('✅ Project initialization complete!');
|
|
49
230
|
}
|
|
50
231
|
|
|
51
232
|
module.exports = { initProject };
|
|
@@ -4,6 +4,7 @@ const MarkdownIt = require('markdown-it');
|
|
|
4
4
|
const matter = require('gray-matter');
|
|
5
5
|
const hljs = require('highlight.js');
|
|
6
6
|
const container = require('markdown-it-container');
|
|
7
|
+
const attrs = require('markdown-it-attrs');
|
|
7
8
|
|
|
8
9
|
const md = new MarkdownIt({
|
|
9
10
|
html: true,
|
|
@@ -23,6 +24,45 @@ const md = new MarkdownIt({
|
|
|
23
24
|
}
|
|
24
25
|
});
|
|
25
26
|
|
|
27
|
+
// Add markdown-it-attrs for image styling and other element attributes
|
|
28
|
+
// This allows for {.class} syntax after elements
|
|
29
|
+
md.use(attrs, {
|
|
30
|
+
// Allow attributes on images and other elements
|
|
31
|
+
leftDelimiter: '{',
|
|
32
|
+
rightDelimiter: '}',
|
|
33
|
+
allowedAttributes: ['class', 'id', 'width', 'height', 'style']
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Custom image renderer to ensure attributes are properly applied
|
|
37
|
+
const defaultImageRenderer = md.renderer.rules.image;
|
|
38
|
+
md.renderer.rules.image = function(tokens, idx, options, env, self) {
|
|
39
|
+
// Get the rendered HTML from the default renderer
|
|
40
|
+
const renderedImage = defaultImageRenderer(tokens, idx, options, env, self);
|
|
41
|
+
|
|
42
|
+
// Check if the next token is an attrs_block
|
|
43
|
+
const nextToken = tokens[idx + 1];
|
|
44
|
+
if (nextToken && nextToken.type === 'attrs_block') {
|
|
45
|
+
// Extract attributes from the attrs_block token
|
|
46
|
+
const attrs = nextToken.attrs || [];
|
|
47
|
+
|
|
48
|
+
// Build the attributes string
|
|
49
|
+
const attrsStr = attrs.map(([name, value]) => {
|
|
50
|
+
if (name === 'class') {
|
|
51
|
+
return `class="${value}"`;
|
|
52
|
+
} else if (name.startsWith('data-')) {
|
|
53
|
+
return `${name}="${value}"`;
|
|
54
|
+
} else {
|
|
55
|
+
return `${name}="${value}"`;
|
|
56
|
+
}
|
|
57
|
+
}).join(' ');
|
|
58
|
+
|
|
59
|
+
// Insert attributes into the image tag
|
|
60
|
+
return renderedImage.replace('<img ', `<img ${attrsStr} `);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return renderedImage;
|
|
64
|
+
};
|
|
65
|
+
|
|
26
66
|
// Add anchors to headings for TOC linking
|
|
27
67
|
md.use((md) => {
|
|
28
68
|
// Original renderer
|