@mgks/docmd 0.2.0 → 0.2.1

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,45 @@
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
  */
9
6
 
10
- // --- 1. Theme Toggle Logic ---
11
- function initializeThemeToggle() {
7
+ // --- Theme Toggle Logic ---
8
+ function setupThemeToggleListener() {
12
9
  const themeToggleButton = document.getElementById('theme-toggle-button');
13
10
 
14
- // Function to apply the theme to the body and save preference
15
- const applyTheme = (theme, isInitial = false) => {
11
+ function applyTheme(theme) {
12
+ document.documentElement.setAttribute('data-theme', theme);
16
13
  document.body.setAttribute('data-theme', theme);
17
- if (!isInitial) {
18
- localStorage.setItem('docmd-theme', theme);
14
+ localStorage.setItem('docmd-theme', theme);
15
+
16
+ // Switch highlight.js theme
17
+ const highlightThemeLink = document.getElementById('highlight-theme');
18
+ if (highlightThemeLink) {
19
+ const newHref = highlightThemeLink.getAttribute('data-base-href') + `docmd-highlight-${theme}.css`;
20
+ highlightThemeLink.setAttribute('href', newHref);
19
21
  }
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);
22
+ }
26
23
 
27
24
  // Add click listener to the toggle button
28
25
  if (themeToggleButton) {
29
26
  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');
27
+ const currentTheme = document.documentElement.getAttribute('data-theme');
28
+ const newTheme = currentTheme === 'light' ? 'dark' : 'light';
34
29
  applyTheme(newTheme);
35
30
  });
36
31
  }
37
32
  }
38
33
 
39
- // --- 2. Sidebar Collapse Logic ---
34
+ // --- Sidebar Collapse Logic ---
40
35
  function initializeSidebarToggle() {
41
36
  const toggleButton = document.getElementById('sidebar-toggle-button');
42
37
  const body = document.body;
43
38
 
44
- // Only run if the sidebar is configured to be collapsible
45
39
  if (!body.classList.contains('sidebar-collapsible') || !toggleButton) {
46
40
  return;
47
41
  }
48
42
 
49
- // Set initial state from localStorage or config default
50
43
  const defaultConfigCollapsed = body.dataset.defaultCollapsed === 'true';
51
44
  let isCollapsed = localStorage.getItem('docmd-sidebar-collapsed');
52
45
 
@@ -60,7 +53,6 @@ function initializeSidebarToggle() {
60
53
  body.classList.add('sidebar-collapsed');
61
54
  }
62
55
 
63
- // Add click listener to the toggle button
64
56
  toggleButton.addEventListener('click', () => {
65
57
  body.classList.toggle('sidebar-collapsed');
66
58
  const currentlyCollapsed = body.classList.contains('sidebar-collapsed');
@@ -68,7 +60,7 @@ function initializeSidebarToggle() {
68
60
  });
69
61
  }
70
62
 
71
- // --- 3. Tabs Container Logic ---
63
+ // --- Tabs Container Logic ---
72
64
  function initializeTabs() {
73
65
  document.querySelectorAll('.docmd-tabs').forEach(tabsContainer => {
74
66
  const navItems = tabsContainer.querySelectorAll('.docmd-tabs-nav-item');
@@ -88,10 +80,78 @@ function initializeTabs() {
88
80
  });
89
81
  }
90
82
 
83
+ // --- Copy Code Button Logic ---
84
+ function initializeCopyCodeButtons() {
85
+ if (document.body.dataset.copyCodeEnabled !== 'true') {
86
+ return;
87
+ }
88
+
89
+ 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>`;
90
+ 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>`;
91
+
92
+ document.querySelectorAll('pre').forEach(preElement => {
93
+ const codeElement = preElement.querySelector('code');
94
+ if (!codeElement) return;
95
+
96
+ // Create a wrapper div around the pre element
97
+ const wrapper = document.createElement('div');
98
+ wrapper.style.position = 'relative';
99
+ wrapper.style.display = 'block';
100
+
101
+ // Insert the wrapper before the pre element
102
+ preElement.parentNode.insertBefore(wrapper, preElement);
103
+
104
+ // Move the pre element into the wrapper
105
+ wrapper.appendChild(preElement);
106
+
107
+ // Remove the relative positioning from pre since wrapper handles it
108
+ preElement.style.position = 'static';
109
+
110
+ const copyButton = document.createElement('button');
111
+ copyButton.className = 'copy-code-button';
112
+ copyButton.innerHTML = copyIconSvg;
113
+ copyButton.setAttribute('aria-label', 'Copy code to clipboard');
114
+ wrapper.appendChild(copyButton);
115
+
116
+ copyButton.addEventListener('click', () => {
117
+ navigator.clipboard.writeText(codeElement.innerText).then(() => {
118
+ copyButton.innerHTML = checkIconSvg;
119
+ copyButton.classList.add('copied');
120
+ setTimeout(() => {
121
+ copyButton.innerHTML = copyIconSvg;
122
+ copyButton.classList.remove('copied');
123
+ }, 2000);
124
+ }).catch(err => {
125
+ console.error('Failed to copy text: ', err);
126
+ copyButton.innerText = 'Error';
127
+ });
128
+ });
129
+ });
130
+ }
131
+
132
+ // --- Theme Sync Function ---
133
+ function syncBodyTheme() {
134
+ const currentTheme = document.documentElement.getAttribute('data-theme');
135
+ if (currentTheme && document.body) {
136
+ document.body.setAttribute('data-theme', currentTheme);
137
+ }
138
+
139
+ // Also ensure highlight CSS matches the current theme
140
+ const highlightThemeLink = document.getElementById('highlight-theme');
141
+ if (highlightThemeLink && currentTheme) {
142
+ const baseHref = highlightThemeLink.getAttribute('data-base-href');
143
+ if (baseHref) {
144
+ const newHref = baseHref + `docmd-highlight-${currentTheme}.css`;
145
+ highlightThemeLink.setAttribute('href', newHref);
146
+ }
147
+ }
148
+ }
91
149
 
92
150
  // --- Main Execution ---
93
151
  document.addEventListener('DOMContentLoaded', () => {
94
- initializeThemeToggle();
152
+ syncBodyTheme(); // Sync body theme with html theme
153
+ setupThemeToggleListener();
95
154
  initializeSidebarToggle();
96
155
  initializeTabs();
156
+ initializeCopyCodeButtons();
97
157
  });
@@ -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;
208
+
209
+ const isIndexFile = path.basename(relativePath) === 'index.md';
199
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,51 +221,26 @@ 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)}`);
226
- }
227
- }
228
-
229
- // Get the URL path for navigation
230
224
  let currentPagePathForNav;
231
225
  let normalizedPath;
232
226
 
233
227
  if (isIndexFile) {
234
- // For index.md files, the nav path should be the directory itself with trailing slash
235
228
  const dirPath = path.dirname(relativePath);
236
229
  if (dirPath === '.') {
237
- // Root index.md
238
230
  currentPagePathForNav = 'index.html';
239
231
  normalizedPath = '/';
240
232
  } else {
241
- // Subfolder index.md - simple format: directory-name/
242
233
  currentPagePathForNav = dirPath + '/';
243
234
  normalizedPath = '/' + dirPath;
244
235
  }
245
236
  } else {
246
- // For non-index files, the path should be the file name with trailing slash
247
237
  const pathWithoutExt = relativePath.replace(/\.md$/, '');
248
238
  currentPagePathForNav = pathWithoutExt + '/';
249
239
  normalizedPath = '/' + pathWithoutExt;
250
240
  }
251
241
 
252
- // Convert Windows backslashes to forward slashes for web paths
253
242
  currentPagePathForNav = currentPagePathForNav.replace(/\\/g, '/');
254
243
 
255
- // Log navigation paths for debugging
256
- // Uncomment this line when debugging:
257
- // logNavigationPaths(filePath, currentPagePathForNav, normalizedPath);
258
-
259
244
  const navigationHtml = await generateNavigationHtml(
260
245
  config.navigation,
261
246
  currentPagePathForNav,
@@ -263,123 +248,80 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
263
248
  config
264
249
  );
265
250
 
266
- // Find current page in navigation for prev/next links
267
251
  let prevPage = null;
268
252
  let nextPage = null;
269
253
  let currentPageIndex = -1;
270
254
 
271
- // Extract a flattened navigation array for prev/next links
272
255
  const flatNavigation = [];
273
256
 
274
- // Helper function to create a normalized path for navigation matching
275
257
  function createNormalizedPath(item) {
276
258
  if (!item.path) return null;
277
259
  return item.path.startsWith('/') ? item.path : '/' + item.path;
278
260
  }
279
261
 
280
- function extractNavigationItems(items, parentPath = '') {
262
+ function extractNavigationItems(items) {
281
263
  if (!items || !Array.isArray(items)) return;
282
264
 
283
265
  for (const item of items) {
284
- if (item.external) continue; // Skip external links
266
+ if (item.external) continue;
285
267
 
286
- // Only include items with paths (not section headers without links)
287
268
  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
- }
269
+ let normalizedItemPath = createNormalizedPath(item);
270
+ if (item.children && !normalizedItemPath.endsWith('/')) {
271
+ normalizedItemPath += '/';
298
272
  }
299
-
300
273
  flatNavigation.push({
301
274
  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
275
+ path: normalizedItemPath,
305
276
  });
306
277
  }
307
278
 
308
- // Process children (depth first to maintain document outline order)
309
279
  if (item.children && Array.isArray(item.children)) {
310
- extractNavigationItems(item.children, item.path || parentPath);
280
+ extractNavigationItems(item.children);
311
281
  }
312
282
  }
313
283
  }
314
284
 
315
- // Extract navigation items into flat array
316
285
  extractNavigationItems(config.navigation);
317
286
 
318
- // Find the current page in flatNavigation
319
287
  currentPageIndex = flatNavigation.findIndex(item => {
320
- // Direct path match
321
- if (item.path === normalizedPath) {
322
- return true;
323
- }
324
-
325
- // Special handling for parent folders
288
+ if (item.path === normalizedPath) return true;
326
289
  if (isIndexFile && item.path.endsWith('/')) {
327
- // Remove trailing slash for comparison
328
- const itemPathWithoutSlash = item.path.slice(0, -1);
329
- return itemPathWithoutSlash === normalizedPath;
290
+ return item.path.slice(0, -1) === normalizedPath;
330
291
  }
331
-
332
292
  return false;
333
293
  });
334
294
 
335
295
  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
- }
296
+ if (currentPageIndex > 0) prevPage = flatNavigation[currentPageIndex - 1];
297
+ if (currentPageIndex < flatNavigation.length - 1) nextPage = flatNavigation[currentPageIndex + 1];
344
298
  }
345
299
 
346
- // Convert page paths to proper URLs for links
347
300
  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
- }
301
+ const cleanPath = prevPage.path.startsWith('/') ? prevPage.path.substring(1) : prevPage.path;
302
+ prevPage.url = relativePathToRoot + (cleanPath.endsWith('/') ? cleanPath : cleanPath + '/');
303
+ if (prevPage.path === '/') prevPage.url = relativePathToRoot;
356
304
  }
357
305
 
358
306
  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
- }
307
+ const cleanPath = nextPage.path.startsWith('/') ? nextPage.path.substring(1) : nextPage.path;
308
+ nextPage.url = relativePathToRoot + (cleanPath.endsWith('/') ? cleanPath : cleanPath + '/');
309
+ if (nextPage.path === '/') nextPage.url = relativePathToRoot;
367
310
  }
368
311
 
369
312
  const pageDataForTemplate = {
370
- content: finalHtmlContent,
313
+ content: htmlContent,
371
314
  pageTitle: pageFrontmatter.title || 'Untitled',
372
315
  siteTitle: config.siteTitle,
373
316
  navigationHtml,
374
317
  relativePathToRoot: relativePathToRoot,
375
- config: config, // Pass full config
318
+ config: config,
376
319
  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
320
+ outputPath: outputHtmlPath.replace(/\\/g, '/'),
321
+ prevPage: prevPage,
322
+ nextPage: nextPage,
323
+ currentPagePath: normalizedPath,
324
+ headings: headings || [],
383
325
  };
384
326
 
385
327
  const pageHtml = await generateHtmlPage(pageDataForTemplate);
@@ -387,16 +329,16 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
387
329
  await fs.ensureDir(path.dirname(finalOutputHtmlPath));
388
330
  await fs.writeFile(finalOutputHtmlPath, pageHtml);
389
331
 
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$/, '/'),
332
+ const sitemapOutputPath = isIndexFile
333
+ ? (path.dirname(relativePath) === '.' ? '' : path.dirname(relativePath) + '/')
334
+ : relativePath.replace(/\.md$/, '/');
335
+
336
+ processedPages.push({
337
+ outputPath: sitemapOutputPath.replace(/\\/g, '/'),
395
338
  frontmatter: pageFrontmatter
396
- };
397
- processedPages.push(processedPage);
339
+ });
398
340
  } catch (error) {
399
- console.error(`Error processing file ${path.relative(CWD, filePath)}:`, error);
341
+ console.error(`❌ An unexpected error occurred while processing file ${path.relative(CWD, filePath)}:`, error);
400
342
  }
401
343
  }
402
344
 
@@ -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');
@@ -34,6 +36,7 @@ module.exports = {
34
36
  defaultMode: 'light', // Initial color mode: 'light' or 'dark'
35
37
  enableModeToggle: true, // Show UI button to toggle light/dark modes
36
38
  positionMode: 'bottom', // '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.
@@ -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