@mgks/docmd 0.2.0 → 0.2.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.
@@ -1,52 +1,118 @@
1
- /**
2
- * docmd-main.js
3
- * Main client-side script for docmd UI interactions.
4
- * Handles:
5
- * 1. Light/Dark theme toggling and persistence.
6
- * 2. Sidebar expand/collapse functionality and persistence.
7
- * 3. Tabs container interaction.
1
+ // Source file from the docmd project — https://github.com/mgks/docmd
2
+
3
+ /*
4
+ * Main client-side script for docmd UI interactions
8
5
  */
6
+ // --- Collapsible Navigation Logic ---
7
+ function initializeCollapsibleNav() {
8
+ const nav = document.querySelector('.sidebar-nav');
9
+ if (!nav) return;
10
+
11
+ let navStates = {};
12
+ try {
13
+ // Use sessionStorage to remember state only for the current session
14
+ navStates = JSON.parse(sessionStorage.getItem('docmd-nav-states')) || {};
15
+ } catch (e) { /* silent fail */ }
16
+
17
+ nav.querySelectorAll('li.collapsible').forEach(item => {
18
+ const navId = item.dataset.navId;
19
+ const anchor = item.querySelector('a');
20
+ const submenu = item.querySelector('.submenu');
21
+
22
+ if (!navId || !anchor || !submenu) return;
23
+
24
+ const isParentActive = item.classList.contains('active-parent');
25
+ // Default to expanded if it's a parent of the active page, otherwise check stored state.
26
+ let isExpanded = isParentActive || (navStates[navId] === true);
27
+
28
+ const toggleSubmenu = (expand) => {
29
+ item.setAttribute('aria-expanded', expand);
30
+ submenu.style.display = expand ? 'block' : 'none';
31
+ navStates[navId] = expand;
32
+ sessionStorage.setItem('docmd-nav-states', JSON.stringify(navStates));
33
+ };
34
+
35
+ // Set initial state on page load
36
+ toggleSubmenu(isExpanded);
37
+
38
+ anchor.addEventListener('click', (e) => {
39
+ // If the click target is the icon, ALWAYS prevent navigation and toggle.
40
+ if (e.target.closest('.collapse-icon')) {
41
+ e.preventDefault();
42
+ toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
43
+ }
44
+ // If the link is just a placeholder, also prevent navigation and toggle.
45
+ else if (anchor.getAttribute('href') === '#') {
46
+ e.preventDefault();
47
+ toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
48
+ }
49
+ // Otherwise, let the click proceed to navigate to the link.
50
+ });
51
+ });
52
+ }
53
+
54
+ // --- Sidebar Scroll Preservation ---
55
+ function initializeSidebarScroll() {
56
+ const sidebar = document.querySelector('.sidebar');
57
+ if (!sidebar) return;
58
+
59
+ setTimeout(() => {
60
+ const activeElement = sidebar.querySelector('a.active') || sidebar.querySelector('.active-parent > a');
61
+
62
+ if (activeElement) {
63
+ const sidebarRect = sidebar.getBoundingClientRect();
64
+ const elementRect = activeElement.getBoundingClientRect();
65
+
66
+ // Check if the element's top or bottom is outside the sidebar's visible area
67
+ const isNotInView = elementRect.top < sidebarRect.top || elementRect.bottom > sidebarRect.bottom;
9
68
 
10
- // --- 1. Theme Toggle Logic ---
11
- function initializeThemeToggle() {
69
+ if (isNotInView) {
70
+ activeElement.scrollIntoView({
71
+ behavior: 'auto',
72
+ block: 'center',
73
+ inline: 'nearest'
74
+ });
75
+ }
76
+ }
77
+ }, 10);
78
+ }
79
+
80
+ // --- Theme Toggle Logic ---
81
+ function setupThemeToggleListener() {
12
82
  const themeToggleButton = document.getElementById('theme-toggle-button');
13
83
 
14
- // Function to apply the theme to the body and save preference
15
- const applyTheme = (theme, isInitial = false) => {
84
+ function applyTheme(theme) {
85
+ document.documentElement.setAttribute('data-theme', theme);
16
86
  document.body.setAttribute('data-theme', theme);
17
- if (!isInitial) {
18
- localStorage.setItem('docmd-theme', theme);
87
+ localStorage.setItem('docmd-theme', theme);
88
+
89
+ // Switch highlight.js theme
90
+ const highlightThemeLink = document.getElementById('highlight-theme');
91
+ if (highlightThemeLink) {
92
+ const newHref = highlightThemeLink.getAttribute('data-base-href') + `docmd-highlight-${theme}.css`;
93
+ highlightThemeLink.setAttribute('href', newHref);
19
94
  }
20
- };
21
-
22
- // Set the initial theme on page load
23
- const storedTheme = localStorage.getItem('docmd-theme');
24
- const initialTheme = storedTheme || document.body.getAttribute('data-theme') || 'light';
25
- applyTheme(initialTheme, true);
95
+ }
26
96
 
27
97
  // Add click listener to the toggle button
28
98
  if (themeToggleButton) {
29
99
  themeToggleButton.addEventListener('click', () => {
30
- const currentTheme = document.body.getAttribute('data-theme');
31
- const newTheme = currentTheme.includes('dark')
32
- ? currentTheme.replace('dark', 'light')
33
- : currentTheme.replace('light', 'dark');
100
+ const currentTheme = document.documentElement.getAttribute('data-theme');
101
+ const newTheme = currentTheme === 'light' ? 'dark' : 'light';
34
102
  applyTheme(newTheme);
35
103
  });
36
104
  }
37
105
  }
38
106
 
39
- // --- 2. Sidebar Collapse Logic ---
107
+ // --- Sidebar Collapse Logic ---
40
108
  function initializeSidebarToggle() {
41
109
  const toggleButton = document.getElementById('sidebar-toggle-button');
42
110
  const body = document.body;
43
111
 
44
- // Only run if the sidebar is configured to be collapsible
45
112
  if (!body.classList.contains('sidebar-collapsible') || !toggleButton) {
46
113
  return;
47
114
  }
48
115
 
49
- // Set initial state from localStorage or config default
50
116
  const defaultConfigCollapsed = body.dataset.defaultCollapsed === 'true';
51
117
  let isCollapsed = localStorage.getItem('docmd-sidebar-collapsed');
52
118
 
@@ -60,7 +126,6 @@ function initializeSidebarToggle() {
60
126
  body.classList.add('sidebar-collapsed');
61
127
  }
62
128
 
63
- // Add click listener to the toggle button
64
129
  toggleButton.addEventListener('click', () => {
65
130
  body.classList.toggle('sidebar-collapsed');
66
131
  const currentlyCollapsed = body.classList.contains('sidebar-collapsed');
@@ -68,7 +133,7 @@ function initializeSidebarToggle() {
68
133
  });
69
134
  }
70
135
 
71
- // --- 3. Tabs Container Logic ---
136
+ // --- Tabs Container Logic ---
72
137
  function initializeTabs() {
73
138
  document.querySelectorAll('.docmd-tabs').forEach(tabsContainer => {
74
139
  const navItems = tabsContainer.querySelectorAll('.docmd-tabs-nav-item');
@@ -88,10 +153,80 @@ function initializeTabs() {
88
153
  });
89
154
  }
90
155
 
156
+ // --- Copy Code Button Logic ---
157
+ function initializeCopyCodeButtons() {
158
+ if (document.body.dataset.copyCodeEnabled !== 'true') {
159
+ return;
160
+ }
161
+
162
+ 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>`;
163
+ 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>`;
164
+
165
+ document.querySelectorAll('pre').forEach(preElement => {
166
+ const codeElement = preElement.querySelector('code');
167
+ if (!codeElement) return;
168
+
169
+ // Create a wrapper div around the pre element
170
+ const wrapper = document.createElement('div');
171
+ wrapper.style.position = 'relative';
172
+ wrapper.style.display = 'block';
173
+
174
+ // Insert the wrapper before the pre element
175
+ preElement.parentNode.insertBefore(wrapper, preElement);
176
+
177
+ // Move the pre element into the wrapper
178
+ wrapper.appendChild(preElement);
179
+
180
+ // Remove the relative positioning from pre since wrapper handles it
181
+ preElement.style.position = 'static';
182
+
183
+ const copyButton = document.createElement('button');
184
+ copyButton.className = 'copy-code-button';
185
+ copyButton.innerHTML = copyIconSvg;
186
+ copyButton.setAttribute('aria-label', 'Copy code to clipboard');
187
+ wrapper.appendChild(copyButton);
188
+
189
+ copyButton.addEventListener('click', () => {
190
+ navigator.clipboard.writeText(codeElement.innerText).then(() => {
191
+ copyButton.innerHTML = checkIconSvg;
192
+ copyButton.classList.add('copied');
193
+ setTimeout(() => {
194
+ copyButton.innerHTML = copyIconSvg;
195
+ copyButton.classList.remove('copied');
196
+ }, 2000);
197
+ }).catch(err => {
198
+ console.error('Failed to copy text: ', err);
199
+ copyButton.innerText = 'Error';
200
+ });
201
+ });
202
+ });
203
+ }
204
+
205
+ // --- Theme Sync Function ---
206
+ function syncBodyTheme() {
207
+ const currentTheme = document.documentElement.getAttribute('data-theme');
208
+ if (currentTheme && document.body) {
209
+ document.body.setAttribute('data-theme', currentTheme);
210
+ }
211
+
212
+ // Also ensure highlight CSS matches the current theme
213
+ const highlightThemeLink = document.getElementById('highlight-theme');
214
+ if (highlightThemeLink && currentTheme) {
215
+ const baseHref = highlightThemeLink.getAttribute('data-base-href');
216
+ if (baseHref) {
217
+ const newHref = baseHref + `docmd-highlight-${currentTheme}.css`;
218
+ highlightThemeLink.setAttribute('href', newHref);
219
+ }
220
+ }
221
+ }
91
222
 
92
223
  // --- Main Execution ---
93
224
  document.addEventListener('DOMContentLoaded', () => {
94
- initializeThemeToggle();
225
+ syncBodyTheme(); // Sync body theme with html theme
226
+ setupThemeToggleListener();
95
227
  initializeSidebarToggle();
96
228
  initializeTabs();
229
+ initializeCopyCodeButtons();
230
+ initializeCollapsibleNav();
231
+ initializeSidebarScroll();
97
232
  });
@@ -1,13 +1,16 @@
1
- // src/commands/build.js
1
+ // Source file from the docmd project — https://github.com/mgks/docmd
2
+
2
3
  const fs = require('fs-extra');
3
4
  const path = require('path');
4
5
  const { loadConfig } = require('../core/config-loader');
5
- const { processMarkdownFile, findMarkdownFiles } = require('../core/file-processor');
6
+ const { createMarkdownItInstance, processMarkdownFile, findMarkdownFiles } = require('../core/file-processor');
6
7
  const { generateHtmlPage, generateNavigationHtml } = require('../core/html-generator');
7
- const { renderIcon, clearWarnedIcons } = require('../core/icon-renderer'); // Update import
8
- const { generateSitemap } = require('../plugins/sitemap'); // Import our sitemap plugin
9
- const { version } = require('../../package.json'); // Import package version
10
- const matter = require('gray-matter'); // Use gray-matter instead of front-matter
8
+ const { renderIcon, clearWarnedIcons } = require('../core/icon-renderer');
9
+ const { generateSitemap } = require('../plugins/sitemap');
10
+ const { version } = require('../../package.json');
11
+ const matter = require('gray-matter');
12
+ const MarkdownIt = require('markdown-it');
13
+ const hljs = require('highlight.js');
11
14
 
12
15
  // Debug function to log navigation information
13
16
  function logNavigationPaths(pagePath, navPath, normalizedPath) {
@@ -55,7 +58,8 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
55
58
  const CWD = process.cwd();
56
59
  const SRC_DIR = path.resolve(CWD, config.srcDir);
57
60
  const OUTPUT_DIR = path.resolve(CWD, config.outputDir);
58
- const USER_ASSETS_DIR = path.resolve(CWD, 'assets'); // User's custom assets directory
61
+ const USER_ASSETS_DIR = path.resolve(CWD, 'assets');
62
+ const md = createMarkdownItInstance(config);
59
63
 
60
64
  if (!await fs.pathExists(SRC_DIR)) {
61
65
  throw new Error(`Source directory not found: ${formatPathForDisplay(SRC_DIR, CWD)}`);
@@ -171,38 +175,44 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
171
175
  // Array to collect information about all processed pages for sitemap
172
176
  const processedPages = [];
173
177
 
178
+ // Set to track processed files for dev mode
179
+ const processedFiles = new Set();
180
+
174
181
  // Find all Markdown files in the source directory
175
182
  const markdownFiles = await findMarkdownFiles(SRC_DIR);
176
183
  if (!options.isDev) {
177
184
  console.log(`📄 Found ${markdownFiles.length} markdown files.`);
178
185
  }
179
-
186
+
180
187
  // Process each Markdown file
181
- const processedFiles = new Set(); // Track processed files to avoid double processing
182
-
183
188
  for (const filePath of markdownFiles) {
184
189
  try {
185
- const fileContent = await fs.readFile(filePath, 'utf8');
186
- const { data: frontmatter, content } = matter(fileContent);
187
-
188
- // Skip this file if it's already been processed and noDoubleProcessing is true
189
190
  const relativePath = path.relative(SRC_DIR, filePath);
191
+
192
+ // Skip file if already processed in this dev build cycle
190
193
  if (options.noDoubleProcessing && processedFiles.has(relativePath)) {
191
194
  continue;
192
195
  }
193
196
  processedFiles.add(relativePath);
197
+
198
+ // Pass the md instance to the processor
199
+ const processedData = await processMarkdownFile(filePath, md, config);
200
+
201
+ // If processing failed (e.g., bad frontmatter), skip this file.
202
+ if (!processedData) {
203
+ continue;
204
+ }
194
205
 
195
- // Pretty URL handling - properly handle index.md files in subfolders
196
- let outputHtmlPath;
197
- const fileName = path.basename(relativePath);
198
- const isIndexFile = fileName === 'index.md';
206
+ // Destructure the valid data
207
+ const { frontmatter: pageFrontmatter, htmlContent, headings } = processedData;
199
208
 
209
+ const isIndexFile = path.basename(relativePath) === 'index.md';
210
+
211
+ let outputHtmlPath;
200
212
  if (isIndexFile) {
201
- // For any index.md file (in root or subfolder), convert to index.html in the same folder
202
213
  const dirPath = path.dirname(relativePath);
203
214
  outputHtmlPath = path.join(dirPath, 'index.html');
204
215
  } else {
205
- // For non-index files, create a folder with index.html
206
216
  outputHtmlPath = relativePath.replace(/\.md$/, '/index.html');
207
217
  }
208
218
 
@@ -211,50 +221,24 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
211
221
  const depth = outputHtmlPath.split(path.sep).length - 1;
212
222
  const relativePathToRoot = depth > 0 ? '../'.repeat(depth) : './';
213
223
 
214
- const { frontmatter: pageFrontmatter, htmlContent, headings } = await processMarkdownFile(filePath, { isDev: options.isDev }, config);
215
-
216
- // Special handling for no-style pages
217
- let finalHtmlContent = htmlContent;
218
- if (pageFrontmatter.noStyle === true) {
219
- // For no-style pages, ensure the HTML content is not escaped
220
- // This is critical for the landing page and custom pages
221
- finalHtmlContent = htmlContent;
222
-
223
- // Log a message for debugging - but only for non-dev mode or verbose logging
224
- if (!options.isDev) {
225
- console.log(`📄 Processing no-style page: ${path.relative(CWD, filePath)}`);
224
+ let normalizedPath = path.relative(SRC_DIR, filePath).replace(/\\/g, '/');
225
+ if (path.basename(normalizedPath) === 'index.md') {
226
+ normalizedPath = path.dirname(normalizedPath);
227
+ if (normalizedPath === '.') {
228
+ normalizedPath = '';
226
229
  }
230
+ } else {
231
+ normalizedPath = normalizedPath.replace(/\.md$/, '');
227
232
  }
228
233
 
229
- // Get the URL path for navigation
230
- let currentPagePathForNav;
231
- let normalizedPath;
232
-
233
- if (isIndexFile) {
234
- // For index.md files, the nav path should be the directory itself with trailing slash
235
- const dirPath = path.dirname(relativePath);
236
- if (dirPath === '.') {
237
- // Root index.md
238
- currentPagePathForNav = 'index.html';
239
- normalizedPath = '/';
240
- } else {
241
- // Subfolder index.md - simple format: directory-name/
242
- currentPagePathForNav = dirPath + '/';
243
- normalizedPath = '/' + dirPath;
244
- }
245
- } else {
246
- // For non-index files, the path should be the file name with trailing slash
247
- const pathWithoutExt = relativePath.replace(/\.md$/, '');
248
- currentPagePathForNav = pathWithoutExt + '/';
249
- normalizedPath = '/' + pathWithoutExt;
234
+ if (!normalizedPath.startsWith('/')) {
235
+ normalizedPath = '/' + normalizedPath;
236
+ }
237
+ if (normalizedPath.length > 1 && !normalizedPath.endsWith('/')) {
238
+ normalizedPath += '/';
250
239
  }
251
-
252
- // Convert Windows backslashes to forward slashes for web paths
253
- currentPagePathForNav = currentPagePathForNav.replace(/\\/g, '/');
254
240
 
255
- // Log navigation paths for debugging
256
- // Uncomment this line when debugging:
257
- // logNavigationPaths(filePath, currentPagePathForNav, normalizedPath);
241
+ const currentPagePathForNav = normalizedPath;
258
242
 
259
243
  const navigationHtml = await generateNavigationHtml(
260
244
  config.navigation,
@@ -263,123 +247,80 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
263
247
  config
264
248
  );
265
249
 
266
- // Find current page in navigation for prev/next links
267
250
  let prevPage = null;
268
251
  let nextPage = null;
269
252
  let currentPageIndex = -1;
270
253
 
271
- // Extract a flattened navigation array for prev/next links
272
254
  const flatNavigation = [];
273
255
 
274
- // Helper function to create a normalized path for navigation matching
275
256
  function createNormalizedPath(item) {
276
257
  if (!item.path) return null;
277
258
  return item.path.startsWith('/') ? item.path : '/' + item.path;
278
259
  }
279
260
 
280
- function extractNavigationItems(items, parentPath = '') {
261
+ function extractNavigationItems(items) {
281
262
  if (!items || !Array.isArray(items)) return;
282
263
 
283
264
  for (const item of items) {
284
- if (item.external) continue; // Skip external links
265
+ if (item.external) continue;
285
266
 
286
- // Only include items with paths (not section headers without links)
287
267
  if (item.path) {
288
- // Normalize path - ensure leading slash
289
- let normalizedPath = createNormalizedPath(item);
290
-
291
- // For parent items with children, ensure path ends with / (folders)
292
- // This helps with matching in the navigation template
293
- if (item.children && item.children.length > 0) {
294
- // If path from config doesn't end with slash, add it
295
- if (!item.path.endsWith('/') && !normalizedPath.endsWith('/')) {
296
- normalizedPath += '/';
297
- }
268
+ let normalizedItemPath = createNormalizedPath(item);
269
+ if (item.children && !normalizedItemPath.endsWith('/')) {
270
+ normalizedItemPath += '/';
298
271
  }
299
-
300
272
  flatNavigation.push({
301
273
  title: item.title,
302
- path: normalizedPath,
303
- fullPath: item.path, // Original path as defined in config
304
- isParent: item.children && item.children.length > 0 // Mark if it's a parent with children
274
+ path: normalizedItemPath,
305
275
  });
306
276
  }
307
277
 
308
- // Process children (depth first to maintain document outline order)
309
278
  if (item.children && Array.isArray(item.children)) {
310
- extractNavigationItems(item.children, item.path || parentPath);
279
+ extractNavigationItems(item.children);
311
280
  }
312
281
  }
313
282
  }
314
283
 
315
- // Extract navigation items into flat array
316
284
  extractNavigationItems(config.navigation);
317
285
 
318
- // Find the current page in flatNavigation
319
286
  currentPageIndex = flatNavigation.findIndex(item => {
320
- // Direct path match
321
- if (item.path === normalizedPath) {
322
- return true;
323
- }
324
-
325
- // Special handling for parent folders
287
+ if (item.path === normalizedPath) return true;
326
288
  if (isIndexFile && item.path.endsWith('/')) {
327
- // Remove trailing slash for comparison
328
- const itemPathWithoutSlash = item.path.slice(0, -1);
329
- return itemPathWithoutSlash === normalizedPath;
289
+ return item.path.slice(0, -1) === normalizedPath;
330
290
  }
331
-
332
291
  return false;
333
292
  });
334
293
 
335
294
  if (currentPageIndex >= 0) {
336
- // Get previous and next pages if they exist
337
- if (currentPageIndex > 0) {
338
- prevPage = flatNavigation[currentPageIndex - 1];
339
- }
340
-
341
- if (currentPageIndex < flatNavigation.length - 1) {
342
- nextPage = flatNavigation[currentPageIndex + 1];
343
- }
295
+ if (currentPageIndex > 0) prevPage = flatNavigation[currentPageIndex - 1];
296
+ if (currentPageIndex < flatNavigation.length - 1) nextPage = flatNavigation[currentPageIndex + 1];
344
297
  }
345
298
 
346
- // Convert page paths to proper URLs for links
347
299
  if (prevPage) {
348
- // Format the previous page URL, avoiding double slashes
349
- if (prevPage.path === '/') {
350
- prevPage.url = relativePathToRoot + 'index.html';
351
- } else {
352
- // Remove leading slash and ensure clean path
353
- const cleanPath = prevPage.path.substring(1).replace(/\/+$/, '');
354
- prevPage.url = relativePathToRoot + cleanPath + '/';
355
- }
300
+ const cleanPath = prevPage.path.startsWith('/') ? prevPage.path.substring(1) : prevPage.path;
301
+ prevPage.url = relativePathToRoot + (cleanPath.endsWith('/') ? cleanPath : cleanPath + '/');
302
+ if (prevPage.path === '/') prevPage.url = relativePathToRoot;
356
303
  }
357
304
 
358
305
  if (nextPage) {
359
- // Format the next page URL, avoiding double slashes
360
- if (nextPage.path === '/') {
361
- nextPage.url = relativePathToRoot + 'index.html';
362
- } else {
363
- // Remove leading slash and ensure clean path
364
- const cleanPath = nextPage.path.substring(1).replace(/\/+$/, '');
365
- nextPage.url = relativePathToRoot + cleanPath + '/';
366
- }
306
+ const cleanPath = nextPage.path.startsWith('/') ? nextPage.path.substring(1) : nextPage.path;
307
+ nextPage.url = relativePathToRoot + (cleanPath.endsWith('/') ? cleanPath : cleanPath + '/');
308
+ if (nextPage.path === '/') nextPage.url = relativePathToRoot;
367
309
  }
368
310
 
369
311
  const pageDataForTemplate = {
370
- content: finalHtmlContent,
312
+ content: htmlContent,
371
313
  pageTitle: pageFrontmatter.title || 'Untitled',
372
314
  siteTitle: config.siteTitle,
373
315
  navigationHtml,
374
316
  relativePathToRoot: relativePathToRoot,
375
- config: config, // Pass full config
317
+ config: config,
376
318
  frontmatter: pageFrontmatter,
377
- outputPath: outputHtmlPath, // Relative path from outputDir root
378
- prettyUrl: true, // Flag to indicate we're using pretty URLs
379
- prevPage: prevPage, // Previous page in navigation
380
- nextPage: nextPage, // Next page in navigation
381
- currentPagePath: normalizedPath, // Pass the normalized path for active state detection
382
- headings: headings || [], // Pass headings for TOC
319
+ outputPath: outputHtmlPath.replace(/\\/g, '/'),
320
+ prevPage: prevPage,
321
+ nextPage: nextPage,
322
+ currentPagePath: normalizedPath,
323
+ headings: headings || [],
383
324
  };
384
325
 
385
326
  const pageHtml = await generateHtmlPage(pageDataForTemplate);
@@ -387,16 +328,16 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
387
328
  await fs.ensureDir(path.dirname(finalOutputHtmlPath));
388
329
  await fs.writeFile(finalOutputHtmlPath, pageHtml);
389
330
 
390
- // Add to processed pages for sitemap
391
- const processedPage = {
392
- outputPath: isIndexFile
393
- ? (path.dirname(relativePath) === '.' ? 'index.html' : path.dirname(relativePath) + '/')
394
- : outputHtmlPath.replace(/\\/g, '/').replace(/\/index\.html$/, '/'),
331
+ const sitemapOutputPath = isIndexFile
332
+ ? (path.dirname(relativePath) === '.' ? '' : path.dirname(relativePath) + '/')
333
+ : relativePath.replace(/\.md$/, '/');
334
+
335
+ processedPages.push({
336
+ outputPath: sitemapOutputPath.replace(/\\/g, '/'),
395
337
  frontmatter: pageFrontmatter
396
- };
397
- processedPages.push(processedPage);
338
+ });
398
339
  } catch (error) {
399
- console.error(`Error processing file ${path.relative(CWD, filePath)}:`, error);
340
+ console.error(`❌ An unexpected error occurred while processing file ${path.relative(CWD, filePath)}:`, error);
400
341
  }
401
342
  }
402
343
 
@@ -1,4 +1,5 @@
1
- // src/commands/dev.js
1
+ // Source file from the docmd project — https://github.com/mgks/docmd
2
+
2
3
  const express = require('express');
3
4
  const http = require('http');
4
5
  const WebSocket = require('ws');
@@ -1,3 +1,5 @@
1
+ // Source file from the docmd project — https://github.com/mgks/docmd
2
+
1
3
  const fs = require('fs-extra');
2
4
  const path = require('path');
3
5
  const readline = require('readline');
@@ -33,7 +35,8 @@ module.exports = {
33
35
  name: 'sky', // Themes: 'default', 'sky'
34
36
  defaultMode: 'light', // Initial color mode: 'light' or 'dark'
35
37
  enableModeToggle: true, // Show UI button to toggle light/dark modes
36
- positionMode: 'bottom', // 'top' or 'bottom' for the theme toggle
38
+ positionMode: 'top', // 'top' or 'bottom' for the theme toggle
39
+ codeHighlight: true, // Enable/disable codeblock highlighting and import of highlight.js
37
40
  customCss: [ // Array of paths to custom CSS files
38
41
  // '/assets/css/custom.css', // Custom TOC styles
39
42
  ]
@@ -47,6 +50,7 @@ module.exports = {
47
50
 
48
51
  // Content Processing
49
52
  autoTitleFromH1: true, // Set to true to automatically use the first H1 as page title
53
+ copyCode: true, // Enable/disable the copy code button on code blocks
50
54
 
51
55
  // Plugins Configuration
52
56
  // Plugins are configured here. docmd will look for these keys.
@@ -92,6 +96,7 @@ module.exports = {
92
96
  title: 'Getting Started',
93
97
  icon: 'rocket',
94
98
  path: '#',
99
+ collapsible: true, // This makes the menu section collapsible
95
100
  children: [
96
101
  { title: 'Documentation', path: 'https://docmd.mgks.dev', icon: 'scroll', external: true },
97
102
  { title: 'Installation', path: 'https://docmd.mgks.dev/getting-started/installation', icon: 'download', external: true },
@@ -1,3 +1,5 @@
1
+ // Source file from the docmd project — https://github.com/mgks/docmd
2
+
1
3
  const path = require('path');
2
4
  const fs = require('fs-extra');
3
5