@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.
- package/LICENSE +1 -1
- package/README.md +61 -63
- package/bin/docmd.js +13 -16
- package/bin/postinstall.js +4 -4
- package/package.json +12 -10
- package/src/assets/css/docmd-highlight-dark.css +86 -1
- package/src/assets/css/docmd-highlight-light.css +86 -1
- package/src/assets/css/docmd-main.css +544 -464
- package/src/assets/css/docmd-theme-retro.css +105 -106
- package/src/assets/css/docmd-theme-ruby.css +92 -92
- package/src/assets/css/docmd-theme-sky.css +63 -64
- package/src/assets/favicon.ico +0 -0
- package/src/assets/images/docmd-logo-dark.png +0 -0
- package/src/assets/images/docmd-logo-light.png +0 -0
- package/src/assets/js/docmd-image-lightbox.js +2 -2
- package/src/assets/js/docmd-main.js +1 -1
- package/src/assets/js/docmd-mermaid.js +1 -1
- package/src/assets/js/docmd-search.js +1 -1
- package/src/commands/build.js +71 -370
- package/src/commands/dev.js +141 -80
- package/src/commands/init.js +107 -132
- package/src/commands/live.js +145 -0
- package/src/core/asset-manager.js +72 -0
- package/src/core/config-loader.js +2 -2
- package/src/core/config-validator.js +1 -1
- package/src/core/file-processor.js +13 -9
- package/src/core/fs-utils.js +40 -0
- package/src/core/html-formatter.js +97 -0
- package/src/core/html-generator.js +61 -65
- package/src/core/icon-renderer.js +1 -1
- package/src/core/logger.js +1 -1
- package/src/core/markdown/containers.js +1 -1
- package/src/core/markdown/renderers.js +1 -1
- package/src/core/markdown/rules.js +1 -2
- package/src/core/markdown/setup.js +1 -1
- package/src/core/navigation-helper.js +1 -1
- package/src/index.js +12 -0
- package/src/live/core.js +5 -1
- package/src/live/index.html +16 -1
- package/src/live/live.css +157 -68
- package/src/plugins/analytics.js +1 -1
- package/src/plugins/seo.js +26 -36
- package/src/plugins/sitemap.js +2 -2
- package/src/templates/layout.ejs +50 -81
- package/src/templates/navigation.ejs +23 -76
- package/src/templates/no-style.ejs +115 -129
- package/src/templates/partials/theme-init.js +1 -1
- package/src/templates/toc.ejs +6 -35
- package/dist/assets/css/docmd-highlight-dark.css +0 -1
- package/dist/assets/css/docmd-highlight-light.css +0 -1
- package/dist/assets/css/docmd-main.css +0 -1627
- package/dist/assets/css/docmd-theme-retro.css +0 -868
- package/dist/assets/css/docmd-theme-ruby.css +0 -629
- package/dist/assets/css/docmd-theme-sky.css +0 -618
- package/dist/assets/favicon.ico +0 -0
- package/dist/assets/images/docmd-logo-dark.png +0 -0
- package/dist/assets/images/docmd-logo-light.png +0 -0
- package/dist/assets/images/docmd-logo.png +0 -0
- package/dist/assets/js/docmd-image-lightbox.js +0 -74
- package/dist/assets/js/docmd-main.js +0 -222
- package/dist/assets/js/docmd-mermaid.js +0 -205
- package/dist/assets/js/docmd-search.js +0 -218
- package/dist/assets/js/mermaid.min.js +0 -2811
- package/dist/assets/js/minisearch.js +0 -2013
- package/dist/docmd-live.js +0 -30748
- package/dist/index.html +0 -201
- package/dist/live.css +0 -167
- package/docmd.config.js +0 -175
- package/scripts/build-live.js +0 -157
- package/scripts/failsafe.js +0 -37
- package/scripts/test-live.js +0 -54
- package/src/assets/images/docmd-logo.png +0 -0
- 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/
|
|
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-
|
|
4
|
+
const fs = require('./fs-utils');
|
|
5
5
|
const { validateConfig } = require('./config-validator');
|
|
6
6
|
|
|
7
7
|
async function loadConfig(configPath) {
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
// Source file from the docmd project — https://github.com/
|
|
1
|
+
// Source file from the docmd project — https://github.com/docmd-io/docmd
|
|
2
2
|
|
|
3
|
-
const fs = require('fs-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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/
|
|
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-
|
|
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
|
-
|
|
17
|
+
try {
|
|
16
18
|
const themeInitPath = path.join(__dirname, '..', 'templates', 'partials', 'theme-init.js');
|
|
17
|
-
if (await fs.
|
|
19
|
+
if (await fs.exists(themeInitPath)) {
|
|
18
20
|
const scriptContent = await fs.readFile(themeInitPath, 'utf8');
|
|
19
|
-
themeInitScript = `<script
|
|
21
|
+
themeInitScript = `<script>\n${scriptContent}\n</script>`;
|
|
20
22
|
}
|
|
21
|
-
}
|
|
23
|
+
} catch (e) { /* ignore */ }
|
|
22
24
|
})();
|
|
23
25
|
|
|
24
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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,
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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')
|
|
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
|
-
|
|
148
|
-
|
|
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,
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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
|
|