@mgks/docmd 0.3.7 → 0.3.8

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 (73) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +61 -63
  3. package/bin/docmd.js +13 -16
  4. package/bin/postinstall.js +4 -4
  5. package/package.json +12 -10
  6. package/src/assets/css/docmd-highlight-dark.css +86 -1
  7. package/src/assets/css/docmd-highlight-light.css +86 -1
  8. package/src/assets/css/docmd-main.css +544 -464
  9. package/src/assets/css/docmd-theme-retro.css +105 -106
  10. package/src/assets/css/docmd-theme-ruby.css +92 -92
  11. package/src/assets/css/docmd-theme-sky.css +63 -64
  12. package/src/assets/favicon.ico +0 -0
  13. package/src/assets/images/docmd-logo-dark.png +0 -0
  14. package/src/assets/images/docmd-logo-light.png +0 -0
  15. package/src/assets/js/docmd-image-lightbox.js +2 -2
  16. package/src/assets/js/docmd-main.js +1 -1
  17. package/src/assets/js/docmd-mermaid.js +1 -1
  18. package/src/assets/js/docmd-search.js +1 -1
  19. package/src/commands/build.js +71 -370
  20. package/src/commands/dev.js +141 -80
  21. package/src/commands/init.js +107 -132
  22. package/src/commands/live.js +145 -0
  23. package/src/core/asset-manager.js +72 -0
  24. package/src/core/config-loader.js +2 -2
  25. package/src/core/config-validator.js +1 -1
  26. package/src/core/file-processor.js +13 -9
  27. package/src/core/fs-utils.js +40 -0
  28. package/src/core/html-formatter.js +97 -0
  29. package/src/core/html-generator.js +61 -65
  30. package/src/core/icon-renderer.js +1 -1
  31. package/src/core/logger.js +1 -1
  32. package/src/core/markdown/containers.js +1 -1
  33. package/src/core/markdown/renderers.js +1 -1
  34. package/src/core/markdown/rules.js +1 -2
  35. package/src/core/markdown/setup.js +1 -1
  36. package/src/core/navigation-helper.js +1 -1
  37. package/src/index.js +12 -0
  38. package/src/live/core.js +5 -1
  39. package/src/live/index.html +16 -1
  40. package/src/live/live.css +157 -68
  41. package/src/plugins/analytics.js +1 -1
  42. package/src/plugins/seo.js +26 -36
  43. package/src/plugins/sitemap.js +2 -2
  44. package/src/templates/layout.ejs +50 -81
  45. package/src/templates/navigation.ejs +23 -76
  46. package/src/templates/no-style.ejs +115 -129
  47. package/src/templates/partials/theme-init.js +1 -1
  48. package/src/templates/toc.ejs +6 -35
  49. package/dist/assets/css/docmd-highlight-dark.css +0 -1
  50. package/dist/assets/css/docmd-highlight-light.css +0 -1
  51. package/dist/assets/css/docmd-main.css +0 -1627
  52. package/dist/assets/css/docmd-theme-retro.css +0 -868
  53. package/dist/assets/css/docmd-theme-ruby.css +0 -629
  54. package/dist/assets/css/docmd-theme-sky.css +0 -618
  55. package/dist/assets/favicon.ico +0 -0
  56. package/dist/assets/images/docmd-logo-dark.png +0 -0
  57. package/dist/assets/images/docmd-logo-light.png +0 -0
  58. package/dist/assets/images/docmd-logo.png +0 -0
  59. package/dist/assets/js/docmd-image-lightbox.js +0 -74
  60. package/dist/assets/js/docmd-main.js +0 -222
  61. package/dist/assets/js/docmd-mermaid.js +0 -205
  62. package/dist/assets/js/docmd-search.js +0 -218
  63. package/dist/assets/js/mermaid.min.js +0 -2811
  64. package/dist/assets/js/minisearch.js +0 -2013
  65. package/dist/docmd-live.js +0 -30748
  66. package/dist/index.html +0 -201
  67. package/dist/live.css +0 -167
  68. package/docmd.config.js +0 -175
  69. package/scripts/build-live.js +0 -157
  70. package/scripts/failsafe.js +0 -37
  71. package/scripts/test-live.js +0 -54
  72. package/src/assets/images/docmd-logo.png +0 -0
  73. package/src/live/templates.js +0 -9
@@ -0,0 +1,145 @@
1
+ // Source file from the docmd project — https://github.com/docmd-io/docmd
2
+
3
+ const fs = require('../core/fs-utils'); // Native wrapper
4
+ const path = require('path');
5
+ const esbuild = require('esbuild');
6
+ const { processAssets } = require('../core/asset-manager');
7
+
8
+ async function build() {
9
+ console.log('📦 Building @docmd/live core...');
10
+
11
+ // Resolve paths relative to src/commands/live.js
12
+ const SRC_DIR = path.resolve(__dirname, '..'); // Points to /src
13
+ const LIVE_SRC_DIR = path.join(SRC_DIR, 'live');
14
+ const DIST_DIR = path.resolve(__dirname, '../../dist'); // Points to /dist in root
15
+
16
+ // 1. Clean/Create dist
17
+ if (await fs.exists(DIST_DIR)) {
18
+ await fs.remove(DIST_DIR);
19
+ }
20
+ await fs.ensureDir(DIST_DIR);
21
+
22
+ // 2. Generate Shim for Buffer (Browser compatibility)
23
+ const shimPath = path.join(LIVE_SRC_DIR, 'shims.js');
24
+ await fs.writeFile(shimPath, `import { Buffer } from 'buffer'; globalThis.Buffer = Buffer;`);
25
+
26
+ // 3. Define the Virtual Template Plugin
27
+ // This reads EJS files from disk and bundles them as a JSON object string
28
+ const templatePlugin = {
29
+ name: 'docmd-templates',
30
+ setup(build) {
31
+ build.onResolve({ filter: /^virtual:docmd-templates$/ }, args => ({
32
+ path: args.path,
33
+ namespace: 'docmd-templates-ns',
34
+ }));
35
+
36
+ build.onLoad({ filter: /.*/, namespace: 'docmd-templates-ns' }, async () => {
37
+ const templatesDir = path.join(SRC_DIR, 'templates');
38
+ const files = await fs.readdir(templatesDir);
39
+ const templates = {};
40
+
41
+ for (const file of files) {
42
+ if (file.endsWith('.ejs')) {
43
+ const content = await fs.readFile(path.join(templatesDir, file), 'utf8');
44
+ templates[file] = content;
45
+ }
46
+ }
47
+
48
+ return {
49
+ contents: `module.exports = ${JSON.stringify(templates)};`,
50
+ loader: 'js',
51
+ };
52
+ });
53
+ },
54
+ };
55
+
56
+ // 4. Define Node Modules Shim Plugin
57
+ // Stubs out fs/path requires so the browser bundle doesn't crash
58
+ const nodeShimPlugin = {
59
+ name: 'node-deps-shim',
60
+ setup(build) {
61
+ build.onResolve({ filter: /^(node:)?path$/ }, args => ({ path: args.path, namespace: 'path-shim' }));
62
+ build.onLoad({ filter: /.*/, namespace: 'path-shim' }, () => ({
63
+ contents: `
64
+ module.exports = {
65
+ join: (...args) => args.filter(Boolean).join('/'),
66
+ resolve: (...args) => '/' + args.filter(Boolean).join('/'),
67
+ basename: (p) => p ? p.split(/[\\\\/]/).pop() : '',
68
+ dirname: (p) => p ? p.split(/[\\\\/]/).slice(0, -1).join('/') || '.' : '.',
69
+ extname: (p) => { if (!p) return ''; const parts = p.split('.'); return parts.length > 1 ? '.' + parts.pop() : ''; },
70
+ isAbsolute: (p) => p.startsWith('/'),
71
+ normalize: (p) => p,
72
+ sep: '/'
73
+ };
74
+ `,
75
+ loader: 'js'
76
+ }));
77
+
78
+ build.onResolve({ filter: /^(node:)?fs(\/promises)?|fs-extra$/ }, args => ({ path: args.path, namespace: 'fs-shim' }));
79
+ build.onLoad({ filter: /.*/, namespace: 'fs-shim' }, () => ({
80
+ contents: `
81
+ module.exports = {
82
+ existsSync: () => false,
83
+ readFileSync: () => '',
84
+ statSync: () => ({ isFile: () => true, isDirectory: () => false }),
85
+ constants: { F_OK: 0, R_OK: 4 },
86
+ promises: {}
87
+ };
88
+ `,
89
+ loader: 'js'
90
+ }));
91
+ }
92
+ };
93
+
94
+ // 5. Bundle JS
95
+ console.log('⚡ Bundling & Compressing JS...');
96
+ try {
97
+ await esbuild.build({
98
+ entryPoints: [path.join(LIVE_SRC_DIR, 'core.js')],
99
+ bundle: true,
100
+ outfile: path.join(DIST_DIR, 'docmd-live.js'),
101
+ platform: 'browser',
102
+ format: 'iife',
103
+ globalName: 'docmd',
104
+ minify: true,
105
+ define: { 'process.env.NODE_ENV': '"production"' },
106
+ inject: [shimPath],
107
+ plugins: [templatePlugin, nodeShimPlugin]
108
+ });
109
+
110
+ // 6. Copy & Minify Static Assets using Universal Manager
111
+ console.log('📂 Processing static assets...');
112
+ const assetsSrc = path.join(SRC_DIR, 'assets');
113
+ const assetsDest = path.join(DIST_DIR, 'assets');
114
+
115
+ if (await fs.exists(assetsSrc)) {
116
+ await processAssets(assetsSrc, assetsDest, { minify: true });
117
+ }
118
+
119
+ // 7. Copy HTML Wrapper & Minify Live CSS
120
+ await fs.copy(path.join(LIVE_SRC_DIR, 'index.html'), path.join(DIST_DIR, 'index.html'));
121
+
122
+ const liveCss = await fs.readFile(path.join(LIVE_SRC_DIR, 'live.css'), 'utf8');
123
+ const minifiedLiveCss = await esbuild.transform(liveCss, { loader: 'css', minify: true });
124
+ await fs.writeFile(path.join(DIST_DIR, 'live.css'), minifiedLiveCss.code);
125
+
126
+ // 8. Copy Favicon to Root
127
+ const internalFavicon = path.join(assetsSrc, 'favicon.ico');
128
+ if (await fs.exists(internalFavicon)) {
129
+ await fs.copy(internalFavicon, path.join(DIST_DIR, 'favicon.ico'));
130
+ }
131
+
132
+ console.log('✅ Live Editor build complete!');
133
+
134
+ } catch (e) {
135
+ console.error('❌ Build failed:', e);
136
+ process.exit(1);
137
+ }
138
+ }
139
+
140
+ // Allow direct execution (via scripts/failsafe.js)
141
+ if (require.main === module) {
142
+ build();
143
+ }
144
+
145
+ module.exports = { build };
@@ -0,0 +1,72 @@
1
+ // Source file from the docmd project — https://github.com/docmd-io/docmd
2
+
3
+ const path = require('path');
4
+ const fs = require('./fs-utils');
5
+ const esbuild = require('esbuild');
6
+
7
+ const DOCMD_HEADER = `/*! Source file from the docmd project — https://github.com/docmd-io/docmd */\n\n`;
8
+
9
+ /**
10
+ * Recursively copies assets from src to dest.
11
+ * Minifies CSS/JS files and adds docmd header.
12
+ *
13
+ * @param {string} srcDir - Source directory
14
+ * @param {string} destDir - Destination directory
15
+ * @param {object} options - { minify: boolean }
16
+ */
17
+ async function processAssets(srcDir, destDir, options = { minify: true }) {
18
+ // Ensure source exists
19
+ if (!await fs.exists(srcDir)) return;
20
+
21
+ // Ensure dest exists
22
+ await fs.ensureDir(destDir);
23
+
24
+ const entries = await fs.readdir(srcDir, { withFileTypes: true });
25
+
26
+ for (const entry of entries) {
27
+ const srcPath = path.join(srcDir, entry.name);
28
+ const destPath = path.join(destDir, entry.name);
29
+
30
+ if (entry.isDirectory()) {
31
+ await processAssets(srcPath, destPath, options);
32
+ } else {
33
+ const ext = path.extname(entry.name).toLowerCase();
34
+
35
+ // Handle CSS & JS: Minify + docmd Header
36
+ if (ext === '.css' || ext === '.js') {
37
+ try {
38
+ const content = await fs.readFile(srcPath, 'utf8');
39
+
40
+ if (options.minify) {
41
+ // Minify using esbuild
42
+ const result = await esbuild.transform(content, {
43
+ loader: ext.slice(1), // 'css' or 'js'
44
+ minify: true,
45
+ banner: ext === '.css' || ext === '.js' ? DOCMD_HEADER : ''
46
+ });
47
+ await fs.writeFile(destPath, result.code);
48
+ } else {
49
+ // Just add banner if not minifying (Dev mode)
50
+ const contentWithBanner = `${DOCMD_HEADER}\n${content}`;
51
+ await fs.writeFile(destPath, contentWithBanner);
52
+ }
53
+ } catch (e) {
54
+ console.warn(`⚠️ Processing failed for ${entry.name}, copying original.`);
55
+ await fs.copy(srcPath, destPath);
56
+ }
57
+ }
58
+ // Handle HTML Files (if any in assets): HTML Comment Header
59
+ else if (ext === '.html') {
60
+ const content = await fs.readFile(srcPath, 'utf8');
61
+ const htmlBanner = `<!-- Generated by docmd - https://docmd.io -->\n\n`;
62
+ await fs.writeFile(destPath, htmlBanner + content);
63
+ }
64
+ // Everything else (Images, Fonts): Direct Copy
65
+ else {
66
+ await fs.copy(srcPath, destPath);
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ module.exports = { processAssets, DOCMD_HEADER };
@@ -1,7 +1,7 @@
1
- // Source file from the docmd project — https://github.com/mgks/docmd
1
+ // Source file from the docmd project — https://github.com/docmd-io/docmd
2
2
 
3
3
  const path = require('path');
4
- const fs = require('fs-extra');
4
+ const fs = require('./fs-utils');
5
5
  const { validateConfig } = require('./config-validator');
6
6
 
7
7
  async function loadConfig(configPath) {
@@ -1,4 +1,4 @@
1
- // Source file from the docmd project — https://github.com/mgks/docmd
1
+ // Source file from the docmd project — https://github.com/docmd-io/docmd
2
2
 
3
3
  const chalk = require('chalk');
4
4
 
@@ -1,14 +1,18 @@
1
- // Source file from the docmd project — https://github.com/mgks/docmd
1
+ // Source file from the docmd project — https://github.com/docmd-io/docmd
2
2
 
3
- const fs = require('fs-extra');
3
+ const fs = require('./fs-utils');
4
4
  const path = require('path');
5
5
  const matter = require('gray-matter');
6
6
  const { createMarkdownItInstance } = require('./markdown/setup');
7
- const striptags = require('striptags');
8
7
 
9
8
  function decodeHtmlEntities(html) {
10
9
  return html.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ');
11
10
  }
11
+
12
+ function stripHtmlTags(str) {
13
+ if (!str) return '';
14
+ return str.replace(/<[^>]*>?/gm, '');
15
+ }
12
16
 
13
17
  function extractHeadingsFromHtml(htmlContent) {
14
18
  const headings = [];
@@ -65,12 +69,12 @@ function processMarkdownContent(rawContent, md, config, filePath = 'memory') {
65
69
 
66
70
  let searchData = null;
67
71
  if (!frontmatter.noindex) {
68
- const rawText = decodeHtmlEntities(striptags(htmlContent));
69
- searchData = {
70
- title: frontmatter.title || 'Untitled',
71
- content: rawText.slice(0, 5000), // Safety cap to prevent massive JSON
72
- headings: headings.map(h => h.text)
73
- };
72
+ const rawText = decodeHtmlEntities(stripHtmlTags(htmlContent));
73
+ searchData = {
74
+ title: frontmatter.title || 'Untitled',
75
+ content: rawText.slice(0, 5000), // Safety cap to prevent massive JSON
76
+ headings: headings.map(h => h.text)
77
+ };
74
78
  }
75
79
 
76
80
  return { frontmatter, htmlContent, headings, searchData };
@@ -0,0 +1,40 @@
1
+ // Source file from the docmd project — https://github.com/docmd-io/docmd
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+
6
+ async function ensureDir(dirPath) {
7
+ await fs.mkdir(dirPath, { recursive: true });
8
+ }
9
+
10
+ async function remove(dirPath) {
11
+ await fs.rm(dirPath, { recursive: true, force: true });
12
+ }
13
+
14
+ async function copy(src, dest) {
15
+ await fs.cp(src, dest, { recursive: true });
16
+ }
17
+
18
+ async function exists(filePath) {
19
+ try {
20
+ await fs.access(filePath);
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ async function writeJson(file, object, options = {}) {
28
+ const content = JSON.stringify(object, null, options.spaces || 2);
29
+ await fs.writeFile(file, content, 'utf8');
30
+ }
31
+
32
+ module.exports = {
33
+ ...fs,
34
+ ensureDir,
35
+ remove,
36
+ copy,
37
+ pathExists: exists,
38
+ exists,
39
+ writeJson
40
+ };
@@ -0,0 +1,97 @@
1
+ // Source file from the docmd project — https://github.com/docmd-io/docmd
2
+
3
+ // Tags that don't have a closing tag (void elements)
4
+ const VOID_TAGS = new Set([
5
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
6
+ 'link', 'meta', 'param', 'source', 'track', 'wbr', '!doctype'
7
+ ]);
8
+
9
+ // Tags where whitespace should be preserved exactly
10
+ const PRESERVE_TAGS = new Set(['pre', 'script', 'style', 'textarea']);
11
+
12
+ function formatHtml(html) {
13
+ if (!html) return '';
14
+
15
+ const lines = html.split(/\r?\n/);
16
+ let formatted = [];
17
+ let indentLevel = 0;
18
+ const indentSize = 4; // 4 spaces per level
19
+
20
+ // State tracking
21
+ let inPreserveBlock = false;
22
+ let preserveTagName = '';
23
+
24
+ for (let i = 0; i < lines.length; i++) {
25
+ let line = lines[i].trim(); // Start with a clean slate for this line
26
+
27
+ if (!line) continue; // Skip empty lines
28
+
29
+ // --- 1. Handle Preserve Blocks (pre/script/style) ---
30
+ if (inPreserveBlock) {
31
+ // Check if this line ends the block
32
+ if (line.toLowerCase().includes(`</${preserveTagName}>`)) {
33
+ inPreserveBlock = false;
34
+ } else {
35
+ // If inside preserve block, add raw line (no trim) from original
36
+ formatted.push(lines[i]);
37
+ continue;
38
+ }
39
+ }
40
+
41
+ // Check if this line STARTS a preserve block
42
+ for (const tag of PRESERVE_TAGS) {
43
+ if (line.toLowerCase().startsWith(`<${tag}`)) {
44
+ inPreserveBlock = true;
45
+ preserveTagName = tag;
46
+ // If it's a one-liner (e.g. <script>...</script>), handle it immediately
47
+ if (line.toLowerCase().includes(`</${tag}>`)) {
48
+ inPreserveBlock = false;
49
+ }
50
+ break;
51
+ }
52
+ }
53
+
54
+ // --- 2. Calculate Indentation ---
55
+
56
+ // Detect closing tags at the start of the line (e.g. </div> or -->)
57
+ const isClosing = /^<\//.test(line) || /^-->/.test(line);
58
+
59
+ if (isClosing && indentLevel > 0) {
60
+ indentLevel--;
61
+ }
62
+
63
+ // Apply indentation
64
+ const spaces = ' '.repeat(indentLevel * indentSize);
65
+ formatted.push(spaces + line);
66
+
67
+ // --- 3. Adjust Indent for Next Line ---
68
+
69
+ // Detect opening tags
70
+ // Logic: Starts with <, not </, not <!, not void tag, and not self-closing />
71
+ const isOpening = /^<[\w]+/.test(line)
72
+ && !/^<\//.test(line)
73
+ && !/\/>$/.test(line); // Ends with />
74
+
75
+ if (isOpening) {
76
+ // Extract tag name to check if it's void
77
+ const tagNameMatch = line.match(/^<([\w-]+)/);
78
+ if (tagNameMatch) {
79
+ const tagName = tagNameMatch[1].toLowerCase();
80
+ if (!VOID_TAGS.has(tagName)) {
81
+ // Check if it's a one-liner: <div>Content</div>
82
+ // Logic: count opening vs closing tags in this line
83
+ // Simple heuristic: if line contains </tagName>, it's closed
84
+ const hasClosingPair = new RegExp(`</${tagName}>`, 'i').test(line);
85
+
86
+ if (!hasClosingPair) {
87
+ indentLevel++;
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ return formatted.join('\n');
95
+ }
96
+
97
+ module.exports = { formatHtml };
@@ -1,70 +1,73 @@
1
- // Source file from the docmd project — https://github.com/mgks/docmd
1
+ // Source file from the docmd project — https://github.com/docmd-io/docmd
2
2
 
3
3
  const ejs = require('ejs');
4
4
  const path = require('path');
5
- const fs = require('fs-extra');
5
+ const fs = require('../core/fs-utils');
6
6
  const { createMarkdownItInstance } = require('./file-processor');
7
7
  const { generateSeoMetaTags } = require('../plugins/seo');
8
8
  const { generateAnalyticsScripts } = require('../plugins/analytics');
9
9
  const { renderIcon } = require('./icon-renderer');
10
+ const { formatHtml } = require('./html-formatter');
10
11
 
11
12
  let mdInstance = null;
12
13
  let themeInitScript = '';
13
14
 
15
+ // Load the theme initialization script into memory once to avoid repeated disk I/O
14
16
  (async () => {
15
- if (typeof __dirname !== 'undefined') {
17
+ try {
16
18
  const themeInitPath = path.join(__dirname, '..', 'templates', 'partials', 'theme-init.js');
17
- if (await fs.pathExists(themeInitPath)) {
19
+ if (await fs.exists(themeInitPath)) {
18
20
  const scriptContent = await fs.readFile(themeInitPath, 'utf8');
19
- themeInitScript = `<script>${scriptContent}</script>`;
21
+ themeInitScript = `<script>\n${scriptContent}\n</script>`;
20
22
  }
21
- }
23
+ } catch (e) { /* ignore */ }
22
24
  })();
23
25
 
24
- // Helper to handle link rewriting based on build mode
26
+ // Removes excessive whitespace and blank lines from the generated HTML
27
+ function cleanupHtml(html) {
28
+ if (!html) return '';
29
+ return html
30
+ .replace(/^[ \t]+$/gm, '')
31
+ .replace(/\n{3,}/g, '\n\n')
32
+ .trim();
33
+ }
34
+
35
+ // Rewrites links based on build mode (Offline/File protocol vs Web Server)
25
36
  function fixHtmlLinks(htmlContent, relativePathToRoot, isOfflineMode) {
26
37
  if (!htmlContent) return '';
27
38
  const root = relativePathToRoot || './';
28
39
 
29
- // Regex matches hrefs starting with /, ./, or ../
30
40
  return htmlContent.replace(/href="((?:\/|\.\/|\.\.\/)[^"]*)"/g, (match, href) => {
31
41
  let finalPath = href;
32
-
33
- // 1. Convert Absolute to Relative
42
+
43
+ // Convert absolute project paths to relative
34
44
  if (href.startsWith('/')) {
35
45
  finalPath = root + href.substring(1);
36
46
  }
37
47
 
38
- // 2. Logic based on Mode
48
+ // Handle offline mode (force index.html for directories)
39
49
  if (isOfflineMode) {
40
- // Offline Mode: Force index.html for directories
41
50
  const cleanPath = finalPath.split('#')[0].split('?')[0];
42
- // If it has no extension (like .html, .css, .png), treat as directory
43
51
  if (!path.extname(cleanPath)) {
44
52
  if (finalPath.includes('#')) {
45
- // Handle anchors: ./foo/#bar -> ./foo/index.html#bar
46
53
  const parts = finalPath.split('#');
47
54
  const prefix = parts[0].endsWith('/') ? parts[0] : parts[0] + '/';
48
55
  finalPath = prefix + 'index.html#' + parts[1];
49
56
  } else {
50
- if (finalPath.endsWith('/')) {
51
- finalPath += 'index.html';
52
- } else {
53
- finalPath += '/index.html';
54
- }
57
+ finalPath += (finalPath.endsWith('/') ? '' : '/') + 'index.html';
55
58
  }
56
59
  }
57
60
  } else {
58
- // Online/Dev Mode: Strip index.html for clean URLs
61
+ // Web mode (strip index.html for clean URLs)
59
62
  if (finalPath.endsWith('/index.html')) {
60
63
  finalPath = finalPath.substring(0, finalPath.length - 10);
61
64
  }
62
65
  }
63
-
64
66
  return `href="${finalPath}"`;
65
67
  });
66
68
  }
67
69
 
70
+ // aggregates HTML snippets from various plugins (SEO, Analytics, etc.)
68
71
  async function processPluginHooks(config, pageData, relativePathToRoot) {
69
72
  let metaTagsHtml = '';
70
73
  let faviconLinkHtml = '';
@@ -74,19 +77,17 @@ async function processPluginHooks(config, pageData, relativePathToRoot) {
74
77
  let pluginBodyScriptsHtml = '';
75
78
 
76
79
  const safeRoot = relativePathToRoot || './';
80
+ const indent = ' '; // 4 spaces for cleaner output
77
81
 
78
- // Favicon
79
82
  if (config.favicon) {
80
83
  const cleanFaviconPath = config.favicon.startsWith('/') ? config.favicon.substring(1) : config.favicon;
81
84
  const finalFaviconHref = `${safeRoot}${cleanFaviconPath}`;
82
-
83
- faviconLinkHtml = ` <link rel="icon" href="${finalFaviconHref}" type="image/x-icon" sizes="any">\n`;
84
- faviconLinkHtml += ` <link rel="shortcut icon" href="${finalFaviconHref}" type="image/x-icon">\n`;
85
+ faviconLinkHtml = `${indent}<link rel="icon" href="${finalFaviconHref}" type="image/x-icon" sizes="any">\n${indent}<link rel="shortcut icon" href="${finalFaviconHref}" type="image/x-icon">`;
85
86
  }
86
87
 
87
88
  if (config.theme && config.theme.name && config.theme.name !== 'default') {
88
89
  const themeCssPath = `assets/css/docmd-theme-${config.theme.name}.css`;
89
- themeCssLinkHtml = ` <link rel="stylesheet" href="${safeRoot}${themeCssPath}">\n`;
90
+ themeCssLinkHtml = `${indent}<link rel="stylesheet" href="${safeRoot}${themeCssPath}">`;
90
91
  }
91
92
 
92
93
  if (config.plugins?.seo) {
@@ -102,91 +103,86 @@ async function processPluginHooks(config, pageData, relativePathToRoot) {
102
103
  return { metaTagsHtml, faviconLinkHtml, themeCssLinkHtml, pluginStylesHtml, pluginHeadScriptsHtml, pluginBodyScriptsHtml };
103
104
  }
104
105
 
106
+ // Main function to assemble the page data and render the EJS template
105
107
  async function generateHtmlPage(templateData, isOfflineMode = false) {
106
- let { content, siteTitle, navigationHtml, relativePathToRoot, config, frontmatter, outputPath, prevPage, nextPage, currentPagePath, headings } = templateData;
108
+ let { content, frontmatter, outputPath, headings, config } = templateData;
109
+ const { currentPagePath, prevPage, nextPage, relativePathToRoot, navigationHtml, siteTitle } = templateData;
107
110
  const pageTitle = frontmatter.title;
108
111
 
109
- if (!relativePathToRoot) relativePathToRoot = './';
112
+ if (!relativePathToRoot) templateData.relativePathToRoot = './';
110
113
 
111
- // Fix Content Links based on mode
112
- content = fixHtmlLinks(content, relativePathToRoot, isOfflineMode);
113
-
114
- const pluginOutputs = await processPluginHooks(config, { frontmatter, outputPath }, relativePathToRoot);
114
+ // Process content links and generate plugin assets
115
+ content = fixHtmlLinks(content, templateData.relativePathToRoot, isOfflineMode);
116
+ const pluginOutputs = await processPluginHooks(config, { frontmatter, outputPath }, templateData.relativePathToRoot);
115
117
 
118
+ // Process footer markdown if present
116
119
  let footerHtml = '';
117
120
  if (config.footer) {
118
121
  if (!mdInstance) mdInstance = createMarkdownItInstance(config);
119
122
  footerHtml = mdInstance.renderInline(config.footer);
120
- // Fix Footer Links based on mode
121
- footerHtml = fixHtmlLinks(footerHtml, relativePathToRoot, isOfflineMode);
122
- }
123
-
124
- let templateName = 'layout.ejs';
125
- if (frontmatter.noStyle === true) {
126
- templateName = 'no-style.ejs';
123
+ footerHtml = fixHtmlLinks(footerHtml, templateData.relativePathToRoot, isOfflineMode);
127
124
  }
128
125
 
126
+ // Determine which template to use
127
+ let templateName = frontmatter.noStyle === true ? 'no-style.ejs' : 'layout.ejs';
129
128
  const layoutTemplatePath = path.join(__dirname, '..', 'templates', templateName);
130
- if (!await fs.pathExists(layoutTemplatePath)) {
131
- throw new Error(`Template not found: ${layoutTemplatePath}`);
132
- }
129
+ if (!await fs.exists(layoutTemplatePath)) throw new Error(`Template not found: ${layoutTemplatePath}`);
133
130
  const layoutTemplate = await fs.readFile(layoutTemplatePath, 'utf8');
134
131
 
135
132
  const isActivePage = currentPagePath && content && content.trim().length > 0;
136
133
 
134
+ // Build the "Edit this page" link
137
135
  let editUrl = null;
138
136
  let editLinkText = 'Edit this page';
139
137
  if (config.editLink && config.editLink.enabled && config.editLink.baseUrl) {
140
- editUrl = `${config.editLink.baseUrl.replace(/\/$/, '')}/${outputPath.replace(/\/index\.html$/, '.md').replace(/\\/g, '/')}`;
138
+ editUrl = `${config.editLink.baseUrl.replace(/\/$/, '')}/${outputPath.replace(/\/index\.html$/, '.md')}`;
141
139
  if (outputPath.endsWith('index.html') && outputPath !== 'index.html') editUrl = editUrl.replace('.md', '/index.md');
142
140
  if (outputPath === 'index.html') editUrl = `${config.editLink.baseUrl.replace(/\/$/, '')}/index.md`;
143
141
  editLinkText = config.editLink.text || editLinkText;
144
142
  }
145
143
 
144
+ // Prepare complete data object for EJS
146
145
  const ejsData = {
147
- content, pageTitle, themeInitScript, description: frontmatter.description, siteTitle, navigationHtml,
148
- editUrl, editLinkText, defaultMode: config.theme?.defaultMode || 'light', relativePathToRoot,
146
+ ...templateData,
147
+ description: frontmatter.description || '', // Fix for reference error
148
+ footerHtml, editUrl, editLinkText, isActivePage,
149
+ defaultMode: config.theme?.defaultMode || 'light',
149
150
  logo: config.logo, sidebarConfig: config.sidebar || {}, theme: config.theme,
150
151
  customCssFiles: config.theme?.customCss || [], customJsFiles: config.customJs || [],
151
- sponsor: config.sponsor, footer: config.footer, footerHtml, renderIcon,
152
- prevPage, nextPage, currentPagePath, headings: frontmatter.toc !== false ? (headings || []) : [],
153
- isActivePage, frontmatter, config, ...pluginOutputs,
152
+ sponsor: config.sponsor, footer: config.footer, renderIcon, themeInitScript,
153
+ headings: frontmatter.toc !== false ? (headings || []) : [],
154
+ ...pluginOutputs,
154
155
  isOfflineMode
155
156
  };
156
157
 
157
- return renderHtmlPage(layoutTemplate, ejsData, layoutTemplatePath);
158
+ // Render and format
159
+ const rawHtml = renderHtmlPage(layoutTemplate, ejsData, layoutTemplatePath);
160
+ const pkgVersion = require('../../package.json').version;
161
+ const brandingComment = `<!-- Generated by docmd (v${pkgVersion}) - https://docmd.io -->\n`;
162
+
163
+ // Apply smart formatting to the final HTML string
164
+ return brandingComment + formatHtml(rawHtml);
158
165
  }
159
166
 
160
167
  function renderHtmlPage(templateContent, ejsData, filename = 'template.ejs', options = {}) {
161
168
  try {
162
- return ejs.render(templateContent, ejsData, {
163
- filename: filename,
164
- ...options
165
- });
169
+ return ejs.render(templateContent, ejsData, { filename: filename, ...options });
166
170
  } catch (e) {
167
171
  console.error(`❌ Error rendering EJS template: ${e.message}`);
168
172
  throw e;
169
173
  }
170
174
  }
171
175
 
172
- // FIX: Added isOfflineMode parameter
176
+ // Generate the sidebar navigation HTML separately
173
177
  async function generateNavigationHtml(navItems, currentPagePath, relativePathToRoot, config, isOfflineMode = false) {
174
178
  const navTemplatePath = path.join(__dirname, '..', 'templates', 'navigation.ejs');
175
- if (!await fs.pathExists(navTemplatePath)) {
176
- throw new Error(`Navigation template not found: ${navTemplatePath}`);
177
- }
179
+ if (!await fs.exists(navTemplatePath)) throw new Error(`Navigation template not found: ${navTemplatePath}`);
178
180
  const navTemplate = await fs.readFile(navTemplatePath, 'utf8');
179
- const ejsHelpers = { renderIcon };
180
-
181
181
  const safeRoot = relativePathToRoot || './';
182
182
 
183
+ // We render raw here; the main page formatter will clean this up later
183
184
  return ejs.render(navTemplate, {
184
- navItems,
185
- currentPagePath,
186
- relativePathToRoot: safeRoot,
187
- config,
188
- isOfflineMode, // <--- Passing the variable here
189
- ...ejsHelpers
185
+ navItems, currentPagePath, relativePathToRoot: safeRoot, config, isOfflineMode, renderIcon
190
186
  }, { filename: navTemplatePath });
191
187
  }
192
188
 
@@ -1,4 +1,4 @@
1
- // Source file from the docmd project — https://github.com/mgks/docmd
1
+ // Source file from the docmd project — https://github.com/docmd-io/docmd
2
2
 
3
3
  const lucideStatic = require('lucide-static');
4
4