@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.
Files changed (31) hide show
  1. package/.github/workflows/publish.yml +1 -1
  2. package/README.md +2 -2
  3. package/bin/docmd.js +8 -2
  4. package/config.js +31 -31
  5. package/docs/cli-commands.md +13 -0
  6. package/docs/content/images.md +205 -0
  7. package/docs/content/index.md +17 -0
  8. package/docs/theming/assets-management.md +126 -0
  9. package/docs/theming/custom-css-js.md +3 -6
  10. package/package.json +2 -1
  11. package/src/assets/css/{main.css → docmd-main.css} +255 -10
  12. package/src/assets/css/{theme-sky.css → docmd-theme-sky.css} +153 -1
  13. package/src/assets/js/docmd-image-lightbox.js +72 -0
  14. package/src/assets/js/{theme-toggle.js → docmd-theme-toggle.js} +4 -4
  15. package/src/commands/build.js +151 -9
  16. package/src/commands/dev.js +103 -17
  17. package/src/commands/init.js +198 -17
  18. package/src/core/file-processor.js +40 -0
  19. package/src/core/html-generator.js +7 -3
  20. package/src/plugins/sitemap.js +10 -3
  21. package/src/templates/layout.ejs +5 -63
  22. package/src/templates/toc.ejs +53 -20
  23. package/docs/writing-content/index.md +0 -17
  24. package/src/assets/css/toc.css +0 -76
  25. /package/docs/{writing-content → content}/custom-containers.md +0 -0
  26. /package/docs/{writing-content → content}/frontmatter.md +0 -0
  27. /package/docs/{writing-content → content}/markdown-syntax.md +0 -0
  28. /package/src/assets/css/{highlight-dark.css → docmd-highlight-dark.css} +0 -0
  29. /package/src/assets/css/{highlight-light.css → docmd-highlight-light.css} +0 -0
  30. /package/src/assets/images/{logo-dark.png → docmd-logo-dark.png} +0 -0
  31. /package/src/assets/images/{logo-light.png → docmd-logo-light.png} +0 -0
@@ -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
- async function buildSite(configPath, options = { isDev: false }) {
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
- await fs.emptyDir(OUTPUT_DIR);
33
- if (!options.isDev) {
34
- console.log(`🧹 Cleaned output directory: ${OUTPUT_DIR}`);
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
- console.log(`📂 Copied assets to ${assetsDestDir}`);
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
@@ -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
- const socket = new WebSocket(\`ws://\${window.location.host}\`);
69
- socket.onmessage = function(event) { if (event.data === 'reload') window.location.reload(); };
70
- socket.onerror = function(error) { console.error('WebSocket Client Error:', error); };
71
- // socket.onopen = function() { console.log('WebSocket Client Connected'); };
72
- // socket.onclose = function() { console.log('WebSocket Client Disconnected'); };
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
- console.log(`👀 Watching for changes in:
108
- - Source: ${paths.srcDirToWatch}
109
- - Config: ${paths.configFileToWatch}
110
- - docmd Templates: ${DOCMD_TEMPLATES_DIR} (internal)
111
- - docmd Assets: ${DOCMD_ASSETS_DIR} (internal)
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 should refresh.');
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
@@ -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
- siteTitle: 'My Awesome Project Docs',
7
- srcDir: 'docs',
8
- outputDir: 'site',
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
- defaultMode: 'light', // 'light' or 'dark'
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
- { title: 'Home', path: '/' }, // Corresponds to docs/index.md
14
- // {
15
- // title: 'Category',
16
- // children: [
17
- // { title: 'Page 1', path: '/category/page1' },
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
- if (await fs.pathExists(configFile) || await fs.pathExists(docsDir)) {
41
- console.warn('⚠️ `docs/` directory or `config.js` already exists. Skipping creation to avoid overwriting.');
42
- } else {
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
- console.log('📁 Created `docs/` directory with a sample `index.md`');
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