@mgks/docmd 0.2.7 → 0.2.9

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.
@@ -20,7 +20,7 @@ function initializeCollapsibleNav() {
20
20
  const submenu = item.querySelector('.submenu');
21
21
 
22
22
  if (!navId || !anchor || !submenu) return;
23
-
23
+
24
24
  const isParentActive = item.classList.contains('active-parent');
25
25
  // Default to expanded if it's a parent of the active page, otherwise check stored state.
26
26
  let isExpanded = isParentActive || (navStates[navId] === true);
@@ -51,19 +51,19 @@ function initializeCollapsibleNav() {
51
51
  }
52
52
  });
53
53
 
54
- /* anchor.addEventListener('click', (e) => {
55
- // If the click target is the icon, ALWAYS prevent navigation and toggle.
56
- if (e.target.closest('.collapse-icon')) {
57
- e.preventDefault();
58
- toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
59
- }
60
- // If the link is just a placeholder, also prevent navigation and toggle.
61
- else if (anchor.getAttribute('href') === '#') {
62
- e.preventDefault();
63
- toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
64
- }
65
- // Otherwise, let the click proceed to navigate to the link.
66
- });*/
54
+ /* anchor.addEventListener('click', (e) => {
55
+ // If the click target is the icon, ALWAYS prevent navigation and toggle.
56
+ if (e.target.closest('.collapse-icon')) {
57
+ e.preventDefault();
58
+ toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
59
+ }
60
+ // If the link is just a placeholder, also prevent navigation and toggle.
61
+ else if (anchor.getAttribute('href') === '#') {
62
+ e.preventDefault();
63
+ toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
64
+ }
65
+ // Otherwise, let the click proceed to navigate to the link.
66
+ });*/
67
67
  });
68
68
  }
69
69
 
@@ -101,7 +101,7 @@ function setupThemeToggleListener() {
101
101
  document.documentElement.setAttribute('data-theme', theme);
102
102
  document.body.setAttribute('data-theme', theme);
103
103
  localStorage.setItem('docmd-theme', theme);
104
-
104
+
105
105
  // Switch highlight.js theme
106
106
  const highlightThemeLink = document.getElementById('highlight-theme');
107
107
  if (highlightThemeLink) {
@@ -131,7 +131,7 @@ function initializeSidebarToggle() {
131
131
 
132
132
  const defaultConfigCollapsed = body.dataset.defaultCollapsed === 'true';
133
133
  let isCollapsed = localStorage.getItem('docmd-sidebar-collapsed');
134
-
134
+
135
135
  if (isCollapsed === null) {
136
136
  isCollapsed = defaultConfigCollapsed;
137
137
  } else {
@@ -161,8 +161,8 @@ function initializeTabs() {
161
161
  tabPanes.forEach(pane => pane.classList.remove('active'));
162
162
 
163
163
  navItem.classList.add('active');
164
- if(tabPanes[index]) {
165
- tabPanes[index].classList.add('active');
164
+ if (tabPanes[index]) {
165
+ tabPanes[index].classList.add('active');
166
166
  }
167
167
  });
168
168
  });
@@ -186,16 +186,16 @@ function initializeCopyCodeButtons() {
186
186
  const wrapper = document.createElement('div');
187
187
  wrapper.style.position = 'relative';
188
188
  wrapper.style.display = 'block';
189
-
189
+
190
190
  // Insert the wrapper before the pre element
191
191
  preElement.parentNode.insertBefore(wrapper, preElement);
192
-
192
+
193
193
  // Move the pre element into the wrapper
194
194
  wrapper.appendChild(preElement);
195
-
195
+
196
196
  // Remove the relative positioning from pre since wrapper handles it
197
197
  preElement.style.position = 'static';
198
-
198
+
199
199
  const copyButton = document.createElement('button');
200
200
  copyButton.className = 'copy-code-button';
201
201
  copyButton.innerHTML = copyIconSvg;
@@ -224,7 +224,7 @@ function syncBodyTheme() {
224
224
  if (currentTheme && document.body) {
225
225
  document.body.setAttribute('data-theme', currentTheme);
226
226
  }
227
-
227
+
228
228
  // Also ensure highlight CSS matches the current theme
229
229
  const highlightThemeLink = document.getElementById('highlight-theme');
230
230
  if (highlightThemeLink && currentTheme) {
@@ -1,13 +1,13 @@
1
1
  // Source file from the docmd project — https://github.com/mgks/docmd
2
2
  // Mermaid diagram integration with theme support
3
3
 
4
- (function() {
4
+ (function () {
5
5
  'use strict';
6
6
 
7
7
  // Configuration for mermaid based on current theme
8
8
  function getMermaidConfig(theme) {
9
9
  const isDark = theme === 'dark';
10
-
10
+
11
11
  return {
12
12
  startOnLoad: false,
13
13
  theme: isDark ? 'dark' : 'default',
@@ -33,16 +33,16 @@
33
33
 
34
34
  const currentTheme = document.body.getAttribute('data-theme') || 'light';
35
35
  const config = getMermaidConfig(currentTheme);
36
-
36
+
37
37
  mermaid.initialize(config);
38
-
38
+
39
39
  // Render all mermaid diagrams
40
40
  renderMermaidDiagrams();
41
41
  }
42
42
 
43
43
  // Store for diagram codes
44
44
  const diagramStore = new Map();
45
-
45
+
46
46
  // Render all mermaid diagrams on the page
47
47
  function renderMermaidDiagrams() {
48
48
  if (typeof mermaid === 'undefined') {
@@ -50,7 +50,7 @@
50
50
  }
51
51
 
52
52
  const mermaidElements = document.querySelectorAll('pre.mermaid');
53
-
53
+
54
54
  mermaidElements.forEach((element, index) => {
55
55
  // Skip if already rendered
56
56
  if (element.getAttribute('data-processed') === 'true') {
@@ -60,22 +60,22 @@
60
60
  try {
61
61
  // Get the diagram code
62
62
  const code = element.textContent;
63
-
63
+
64
64
  // Create a unique ID for this diagram
65
65
  const id = `mermaid-diagram-${index}-${Date.now()}`;
66
-
66
+
67
67
  // Store the original code for re-rendering
68
68
  diagramStore.set(id, code);
69
-
69
+
70
70
  // Create a container div
71
71
  const container = document.createElement('div');
72
72
  container.className = 'mermaid-container';
73
73
  container.setAttribute('data-mermaid-id', id);
74
74
  container.setAttribute('data-processed', 'true');
75
-
75
+
76
76
  // Replace the pre element with the container
77
77
  element.parentNode.replaceChild(container, element);
78
-
78
+
79
79
  // Render the diagram
80
80
  renderSingleDiagram(container, id, code);
81
81
  } catch (error) {
@@ -83,17 +83,17 @@
83
83
  }
84
84
  });
85
85
  }
86
-
86
+
87
87
  // Render a single diagram
88
88
  function renderSingleDiagram(container, id, code) {
89
89
  if (typeof mermaid === 'undefined') {
90
90
  return;
91
91
  }
92
-
92
+
93
93
  // Process the code to handle theme overrides
94
94
  const currentTheme = document.body.getAttribute('data-theme') || 'light';
95
95
  const processedCode = processThemeInCode(code, currentTheme);
96
-
96
+
97
97
  // Render the diagram
98
98
  mermaid.render(id, processedCode).then(result => {
99
99
  container.innerHTML = result.svg;
@@ -102,21 +102,21 @@
102
102
  container.innerHTML = `<pre class="mermaid-error">Error rendering diagram: ${error.message}</pre>`;
103
103
  });
104
104
  }
105
-
105
+
106
106
  // Process mermaid code to inject or override theme
107
107
  function processThemeInCode(code, theme) {
108
108
  const isDark = theme === 'dark';
109
109
  const targetTheme = isDark ? 'dark' : 'default';
110
-
110
+
111
111
  // Check if code has %%{init: config - match the entire init block including nested objects
112
112
  const initRegex = /%%\{init:\s*\{.*?\}\s*\}%%/s;
113
113
  const match = code.match(initRegex);
114
-
114
+
115
115
  if (match) {
116
116
  // Code has init config, replace only the theme property
117
117
  const initBlock = match[0];
118
118
  let updatedBlock = initBlock;
119
-
119
+
120
120
  // Try to replace theme property
121
121
  if (initBlock.includes("'theme'")) {
122
122
  updatedBlock = initBlock.replace(/'theme'\s*:\s*'[^']*'/, `'theme':'${targetTheme}'`);
@@ -126,10 +126,10 @@
126
126
  // Add theme to the config - insert after the first {
127
127
  updatedBlock = initBlock.replace(/%%\{init:\s*\{/, `%%{init: {'theme':'${targetTheme}',`);
128
128
  }
129
-
129
+
130
130
  return code.replace(initRegex, updatedBlock);
131
131
  }
132
-
132
+
133
133
  // No init config, code will use global mermaid config
134
134
  return code;
135
135
  }
@@ -142,21 +142,21 @@
142
142
 
143
143
  const currentTheme = document.body.getAttribute('data-theme') || 'light';
144
144
  const config = getMermaidConfig(currentTheme);
145
-
145
+
146
146
  // Re-initialize mermaid with new theme
147
147
  mermaid.initialize(config);
148
-
148
+
149
149
  // Find all rendered diagrams and re-render them
150
150
  const containers = document.querySelectorAll('.mermaid-container[data-processed="true"]');
151
-
151
+
152
152
  containers.forEach((container) => {
153
153
  const mermaidId = container.getAttribute('data-mermaid-id');
154
154
  const code = diagramStore.get(mermaidId);
155
-
155
+
156
156
  if (code) {
157
157
  // Create a new unique ID for re-rendering
158
158
  const newId = `${mermaidId}-${Date.now()}`;
159
-
159
+
160
160
  // Clear the container and re-render
161
161
  container.innerHTML = '';
162
162
  renderSingleDiagram(container, newId, code);
@@ -182,7 +182,7 @@
182
182
 
183
183
  // Initialize when DOM is ready
184
184
  if (document.readyState === 'loading') {
185
- document.addEventListener('DOMContentLoaded', function() {
185
+ document.addEventListener('DOMContentLoaded', function () {
186
186
  initializeMermaid();
187
187
  setupThemeObserver();
188
188
  });
@@ -192,12 +192,11 @@
192
192
  }
193
193
 
194
194
  // Handle tab switches - render mermaid in newly visible tabs
195
- document.addEventListener('click', function(e) {
195
+ document.addEventListener('click', function (e) {
196
196
  if (e.target.classList.contains('docmd-tabs-nav-item')) {
197
197
  // Wait a bit for tab content to be visible
198
198
  setTimeout(renderMermaidDiagrams, 100);
199
199
  }
200
200
  });
201
201
 
202
- })();
203
-
202
+ })();
@@ -12,6 +12,8 @@ const { version } = require('../../package.json');
12
12
  const matter = require('gray-matter');
13
13
  const MarkdownIt = require('markdown-it');
14
14
  const hljs = require('highlight.js');
15
+ const CleanCSS = require('clean-css');
16
+ const esbuild = require('esbuild');
15
17
 
16
18
  // Debug function to log navigation information
17
19
  function logNavigationPaths(pagePath, navPath, normalizedPath) {
@@ -42,12 +44,12 @@ const ASSET_VERSIONS = {
42
44
  function formatPathForDisplay(absolutePath, cwd) {
43
45
  // Get the relative path from CWD
44
46
  const relativePath = path.relative(cwd, absolutePath);
45
-
47
+
46
48
  // If it's not a subdirectory, prefix with ./ for clarity
47
49
  if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
48
50
  return `./${relativePath}`;
49
51
  }
50
-
52
+
51
53
  // Return the relative path
52
54
  return relativePath;
53
55
  }
@@ -61,6 +63,7 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
61
63
  const OUTPUT_DIR = path.resolve(CWD, config.outputDir);
62
64
  const USER_ASSETS_DIR = path.resolve(CWD, 'assets');
63
65
  const md = createMarkdownItInstance(config);
66
+ const shouldMinify = !options.isDev && config.minify !== false;
64
67
 
65
68
  if (!await fs.pathExists(SRC_DIR)) {
66
69
  throw new Error(`Source directory not found: ${formatPathForDisplay(SRC_DIR, CWD)}`);
@@ -85,27 +88,70 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
85
88
  const preservedFiles = [];
86
89
  const userAssetsCopied = [];
87
90
 
91
+ // Function to process and copy a single asset
92
+ const processAndCopyAsset = async (srcPath, destPath) => {
93
+ const ext = path.extname(srcPath).toLowerCase();
94
+
95
+ if (process.env.DOCMD_DEBUG) {
96
+ console.log(`[Asset Debug] Processing: ${path.basename(srcPath)} | Minify: ${shouldMinify} | Ext: ${ext}`);
97
+ }
98
+
99
+ if (shouldMinify && ext === '.css') {
100
+ try {
101
+ const content = await fs.readFile(srcPath, 'utf8');
102
+ const output = new CleanCSS({}).minify(content);
103
+ if (output.errors.length > 0) {
104
+ console.warn(`⚠️ CSS Minification error for ${path.basename(srcPath)}, using original.`, output.errors);
105
+ await fs.copyFile(srcPath, destPath);
106
+ } else {
107
+ await fs.writeFile(destPath, output.styles);
108
+ }
109
+ } catch (e) {
110
+ console.warn(`⚠️ CSS processing failed: ${e.message}`);
111
+ await fs.copyFile(srcPath, destPath);
112
+ }
113
+ }
114
+ else if (shouldMinify && ext === '.js') {
115
+ try {
116
+ const content = await fs.readFile(srcPath, 'utf8');
117
+ // Simple minification transform
118
+ const result = await esbuild.transform(content, {
119
+ minify: true,
120
+ loader: 'js',
121
+ target: 'es2015' // Ensure compatibility
122
+ });
123
+ await fs.writeFile(destPath, result.code);
124
+ } catch (e) {
125
+ console.warn(`⚠️ JS Minification failed: ${e.message}`);
126
+ await fs.copyFile(srcPath, destPath);
127
+ }
128
+ }
129
+ else {
130
+ // Images, fonts, or dev mode -> standard copy
131
+ await fs.copyFile(srcPath, destPath);
132
+ }
133
+ };
134
+
88
135
  // Copy user assets from root assets/ directory if it exists
89
136
  if (await fs.pathExists(USER_ASSETS_DIR)) {
90
137
  const assetsDestDir = path.join(OUTPUT_DIR, 'assets');
91
138
  await fs.ensureDir(assetsDestDir);
92
-
139
+
93
140
  if (!options.isDev) {
94
141
  console.log(`📂 Copying user assets from ${formatPathForDisplay(USER_ASSETS_DIR, CWD)} to ${formatPathForDisplay(assetsDestDir, CWD)}...`);
95
142
  }
96
-
143
+
97
144
  const userAssetFiles = await getAllFiles(USER_ASSETS_DIR);
98
-
99
145
  for (const srcFile of userAssetFiles) {
100
146
  const relativePath = path.relative(USER_ASSETS_DIR, srcFile);
101
147
  const destFile = path.join(assetsDestDir, relativePath);
102
-
103
- // Ensure directory exists
148
+
104
149
  await fs.ensureDir(path.dirname(destFile));
105
- await fs.copyFile(srcFile, destFile);
150
+ await processAndCopyAsset(srcFile, destFile);
151
+
106
152
  userAssetsCopied.push(relativePath);
107
153
  }
108
-
154
+
109
155
  if (!options.isDev && userAssetsCopied.length > 0) {
110
156
  console.log(`📦 Copied ${userAssetsCopied.length} user assets`);
111
157
  }
@@ -114,41 +160,34 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
114
160
  // Copy assets
115
161
  const assetsSrcDir = path.join(__dirname, '..', 'assets');
116
162
  const assetsDestDir = path.join(OUTPUT_DIR, 'assets');
117
-
163
+
118
164
  if (await fs.pathExists(assetsSrcDir)) {
119
165
  if (!options.isDev) {
120
166
  console.log(`📂 Copying docmd assets to ${formatPathForDisplay(assetsDestDir, CWD)}...`);
121
167
  }
122
-
168
+
123
169
  // Create destination directory if it doesn't exist
124
170
  await fs.ensureDir(assetsDestDir);
125
-
171
+
126
172
  // Get all files from source directory recursively
127
173
  const assetFiles = await getAllFiles(assetsSrcDir);
128
-
129
- // Copy each file individually, checking for existing files if preserve flag is set
130
174
  for (const srcFile of assetFiles) {
131
175
  const relativePath = path.relative(assetsSrcDir, srcFile);
132
176
  const destFile = path.join(assetsDestDir, relativePath);
133
-
134
- // Check if destination file already exists
135
177
  const fileExists = await fs.pathExists(destFile);
136
-
137
- // Skip if the file exists and either:
138
- // 1. The preserve flag is set, OR
139
- // 2. The file was copied from user assets (user assets take precedence)
178
+
140
179
  if (fileExists && (options.preserve || userAssetsCopied.includes(relativePath))) {
141
- // Skip file and add to preserved list
142
180
  preservedFiles.push(relativePath);
143
181
  if (!options.isDev && options.preserve) {
144
182
  console.log(` Preserving existing file: ${relativePath}`);
145
183
  }
184
+
146
185
  } else {
147
- // Copy file (either it doesn't exist or we're not preserving)
148
186
  await fs.ensureDir(path.dirname(destFile));
149
- await fs.copyFile(srcFile, destFile);
187
+ await processAndCopyAsset(srcFile, destFile);
150
188
  }
151
189
  }
190
+
152
191
  } else {
153
192
  console.warn(`⚠️ Assets source directory not found: ${formatPathForDisplay(assetsSrcDir, CWD)}`);
154
193
  }
@@ -189,7 +228,7 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
189
228
  for (const filePath of markdownFiles) {
190
229
  try {
191
230
  const relativePath = path.relative(SRC_DIR, filePath);
192
-
231
+
193
232
  // Skip file if already processed in this dev build cycle
194
233
  if (options.noDoubleProcessing && processedFiles.has(relativePath)) {
195
234
  continue;
@@ -203,12 +242,12 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
203
242
  if (!processedData) {
204
243
  continue;
205
244
  }
206
-
245
+
207
246
  // Destructure the valid data
208
247
  const { frontmatter: pageFrontmatter, htmlContent, headings } = processedData;
209
-
248
+
210
249
  const isIndexFile = path.basename(relativePath) === 'index.md';
211
-
250
+
212
251
  let outputHtmlPath;
213
252
  if (isIndexFile) {
214
253
  const dirPath = path.dirname(relativePath);
@@ -221,9 +260,9 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
221
260
 
222
261
  let relativePathToRoot = path.relative(path.dirname(finalOutputHtmlPath), OUTPUT_DIR);
223
262
  if (relativePathToRoot === '') {
224
- relativePathToRoot = './';
263
+ relativePathToRoot = './';
225
264
  } else {
226
- relativePathToRoot = relativePathToRoot.replace(/\\/g, '/') + '/';
265
+ relativePathToRoot = relativePathToRoot.replace(/\\/g, '/') + '/';
227
266
  }
228
267
 
229
268
  let normalizedPath = path.relative(SRC_DIR, filePath).replace(/\\/g, '/');
@@ -259,7 +298,7 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
259
298
  const cleanPath = prevPage.path.substring(1);
260
299
  prevPage.url = relativePathToRoot + cleanPath;
261
300
  }
262
-
301
+
263
302
  if (nextPage) {
264
303
  const cleanPath = nextPage.path.substring(1);
265
304
  nextPage.url = relativePathToRoot + cleanPath;
@@ -286,9 +325,9 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
286
325
  await fs.ensureDir(path.dirname(finalOutputHtmlPath));
287
326
  await fs.writeFile(finalOutputHtmlPath, pageHtml);
288
327
 
289
- const sitemapOutputPath = isIndexFile
290
- ? (path.dirname(relativePath) === '.' ? '' : path.dirname(relativePath) + '/')
291
- : relativePath.replace(/\.md$/, '/');
328
+ const sitemapOutputPath = isIndexFile
329
+ ? (path.dirname(relativePath) === '.' ? '' : path.dirname(relativePath) + '/')
330
+ : relativePath.replace(/\.md$/, '/');
292
331
 
293
332
  processedPages.push({
294
333
  outputPath: sitemapOutputPath.replace(/\\/g, '/'),
@@ -314,7 +353,7 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
314
353
  preservedFiles.forEach(file => console.log(` - assets/${file}`));
315
354
  console.log(`\nTo update these files in future builds, run without the --preserve flag.`);
316
355
  }
317
-
356
+
318
357
  if (userAssetsCopied.length > 0 && !options.isDev) {
319
358
  console.log(`\n📋 User Assets: ${userAssetsCopied.length} files were copied from your assets/ directory:`);
320
359
  if (userAssetsCopied.length <= 10) {
@@ -336,10 +375,10 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
336
375
  async function findFilesToCleanup(dir) {
337
376
  const filesToRemove = [];
338
377
  const items = await fs.readdir(dir, { withFileTypes: true });
339
-
378
+
340
379
  for (const item of items) {
341
380
  const fullPath = path.join(dir, item.name);
342
-
381
+
343
382
  if (item.isDirectory()) {
344
383
  // Don't delete the assets directory
345
384
  if (item.name !== 'assets') {
@@ -347,13 +386,13 @@ async function findFilesToCleanup(dir) {
347
386
  filesToRemove.push(...subDirFiles);
348
387
  }
349
388
  } else if (
350
- item.name.endsWith('.html') ||
389
+ item.name.endsWith('.html') ||
351
390
  item.name === 'sitemap.xml'
352
391
  ) {
353
392
  filesToRemove.push(fullPath);
354
393
  }
355
394
  }
356
-
395
+
357
396
  return filesToRemove;
358
397
  }
359
398
 
@@ -361,7 +400,7 @@ async function findFilesToCleanup(dir) {
361
400
  async function getAllFiles(dir) {
362
401
  const files = [];
363
402
  const items = await fs.readdir(dir, { withFileTypes: true });
364
-
403
+
365
404
  for (const item of items) {
366
405
  const fullPath = path.join(dir, item.name);
367
406
  if (item.isDirectory()) {
@@ -370,7 +409,7 @@ async function getAllFiles(dir) {
370
409
  files.push(fullPath);
371
410
  }
372
411
  }
373
-
412
+
374
413
  return files;
375
414
  }
376
415
 
@@ -23,6 +23,7 @@ module.exports = {
23
23
  // Directory Configuration
24
24
  srcDir: 'docs', // Source directory for Markdown files
25
25
  outputDir: 'site', // Directory for generated static site
26
+ minify: true, // Enable/disable HTML/CSS/JS minification
26
27
 
27
28
  // Sidebar Configuration
28
29
  sidebar: {
@@ -88,6 +89,15 @@ module.exports = {
88
89
  // Add other future plugin configurations here by their key
89
90
  },
90
91
 
92
+ // "Edit this page" Link Configuration
93
+ editLink: {
94
+ enabled: false,
95
+ // The URL to the folder containing your docs in the git repo
96
+ // Note: It usually ends with /edit/main/docs or /blob/main/docs
97
+ baseUrl: 'https://github.com/mgks/docmd/edit/main/docs',
98
+ text: 'Edit this page on GitHub'
99
+ },
100
+
91
101
  // Navigation Structure (Sidebar)
92
102
  // Icons are kebab-case names from Lucide Icons (https://lucide.dev/)
93
103
  navigation: [