@mgks/docmd 0.2.8 → 0.3.0
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/README.md +3 -1
- package/assets/images/preview-dark-welcome.png +0 -0
- package/config.js +139 -161
- package/docs/comparison.md +5 -0
- package/docs/configuration.md +35 -1
- package/docs/content/markdown-syntax.md +32 -0
- package/docs/content/search.md +68 -0
- package/docs/contributing.md +8 -0
- package/docs/overview.md +1 -0
- package/package.json +7 -2
- package/src/assets/css/docmd-main.css +681 -341
- package/src/assets/css/docmd-theme-retro.css +860 -1
- package/src/assets/css/docmd-theme-ruby.css +621 -1
- package/src/assets/css/docmd-theme-sky.css +610 -1
- package/src/assets/js/docmd-image-lightbox.js +13 -13
- package/src/assets/js/docmd-main.js +24 -23
- package/src/assets/js/docmd-mermaid.js +32 -30
- package/src/assets/js/docmd-search.js +212 -0
- package/src/commands/build.js +195 -41
- package/src/commands/init.js +15 -0
- package/src/core/file-processor.js +12 -1
- package/src/core/html-generator.js +23 -0
- package/src/core/markdown/setup.js +27 -11
- package/src/templates/layout.ejs +55 -3
package/src/commands/build.js
CHANGED
|
@@ -12,6 +12,9 @@ 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');
|
|
17
|
+
const MiniSearch = require('minisearch');
|
|
15
18
|
|
|
16
19
|
// Debug function to log navigation information
|
|
17
20
|
function logNavigationPaths(pagePath, navPath, normalizedPath) {
|
|
@@ -42,12 +45,12 @@ const ASSET_VERSIONS = {
|
|
|
42
45
|
function formatPathForDisplay(absolutePath, cwd) {
|
|
43
46
|
// Get the relative path from CWD
|
|
44
47
|
const relativePath = path.relative(cwd, absolutePath);
|
|
45
|
-
|
|
48
|
+
|
|
46
49
|
// If it's not a subdirectory, prefix with ./ for clarity
|
|
47
50
|
if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
|
|
48
51
|
return `./${relativePath}`;
|
|
49
52
|
}
|
|
50
|
-
|
|
53
|
+
|
|
51
54
|
// Return the relative path
|
|
52
55
|
return relativePath;
|
|
53
56
|
}
|
|
@@ -61,6 +64,8 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
61
64
|
const OUTPUT_DIR = path.resolve(CWD, config.outputDir);
|
|
62
65
|
const USER_ASSETS_DIR = path.resolve(CWD, 'assets');
|
|
63
66
|
const md = createMarkdownItInstance(config);
|
|
67
|
+
const shouldMinify = !options.isDev && config.minify !== false;
|
|
68
|
+
const searchIndexData = [];
|
|
64
69
|
|
|
65
70
|
if (!await fs.pathExists(SRC_DIR)) {
|
|
66
71
|
throw new Error(`Source directory not found: ${formatPathForDisplay(SRC_DIR, CWD)}`);
|
|
@@ -85,27 +90,70 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
85
90
|
const preservedFiles = [];
|
|
86
91
|
const userAssetsCopied = [];
|
|
87
92
|
|
|
93
|
+
// Function to process and copy a single asset
|
|
94
|
+
const processAndCopyAsset = async (srcPath, destPath) => {
|
|
95
|
+
const ext = path.extname(srcPath).toLowerCase();
|
|
96
|
+
|
|
97
|
+
if (process.env.DOCMD_DEBUG) {
|
|
98
|
+
console.log(`[Asset Debug] Processing: ${path.basename(srcPath)} | Minify: ${shouldMinify} | Ext: ${ext}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (shouldMinify && ext === '.css') {
|
|
102
|
+
try {
|
|
103
|
+
const content = await fs.readFile(srcPath, 'utf8');
|
|
104
|
+
const output = new CleanCSS({}).minify(content);
|
|
105
|
+
if (output.errors.length > 0) {
|
|
106
|
+
console.warn(`⚠️ CSS Minification error for ${path.basename(srcPath)}, using original.`, output.errors);
|
|
107
|
+
await fs.copyFile(srcPath, destPath);
|
|
108
|
+
} else {
|
|
109
|
+
await fs.writeFile(destPath, output.styles);
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.warn(`⚠️ CSS processing failed: ${e.message}`);
|
|
113
|
+
await fs.copyFile(srcPath, destPath);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (shouldMinify && ext === '.js') {
|
|
117
|
+
try {
|
|
118
|
+
const content = await fs.readFile(srcPath, 'utf8');
|
|
119
|
+
// Simple minification transform
|
|
120
|
+
const result = await esbuild.transform(content, {
|
|
121
|
+
minify: true,
|
|
122
|
+
loader: 'js',
|
|
123
|
+
target: 'es2015' // Ensure compatibility
|
|
124
|
+
});
|
|
125
|
+
await fs.writeFile(destPath, result.code);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.warn(`⚠️ JS Minification failed: ${e.message}`);
|
|
128
|
+
await fs.copyFile(srcPath, destPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Images, fonts, or dev mode -> standard copy
|
|
133
|
+
await fs.copyFile(srcPath, destPath);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
88
137
|
// Copy user assets from root assets/ directory if it exists
|
|
89
138
|
if (await fs.pathExists(USER_ASSETS_DIR)) {
|
|
90
139
|
const assetsDestDir = path.join(OUTPUT_DIR, 'assets');
|
|
91
140
|
await fs.ensureDir(assetsDestDir);
|
|
92
|
-
|
|
141
|
+
|
|
93
142
|
if (!options.isDev) {
|
|
94
143
|
console.log(`📂 Copying user assets from ${formatPathForDisplay(USER_ASSETS_DIR, CWD)} to ${formatPathForDisplay(assetsDestDir, CWD)}...`);
|
|
95
144
|
}
|
|
96
|
-
|
|
145
|
+
|
|
97
146
|
const userAssetFiles = await getAllFiles(USER_ASSETS_DIR);
|
|
98
|
-
|
|
99
147
|
for (const srcFile of userAssetFiles) {
|
|
100
148
|
const relativePath = path.relative(USER_ASSETS_DIR, srcFile);
|
|
101
149
|
const destFile = path.join(assetsDestDir, relativePath);
|
|
102
|
-
|
|
103
|
-
// Ensure directory exists
|
|
150
|
+
|
|
104
151
|
await fs.ensureDir(path.dirname(destFile));
|
|
105
|
-
await
|
|
152
|
+
await processAndCopyAsset(srcFile, destFile);
|
|
153
|
+
|
|
106
154
|
userAssetsCopied.push(relativePath);
|
|
107
155
|
}
|
|
108
|
-
|
|
156
|
+
|
|
109
157
|
if (!options.isDev && userAssetsCopied.length > 0) {
|
|
110
158
|
console.log(`📦 Copied ${userAssetsCopied.length} user assets`);
|
|
111
159
|
}
|
|
@@ -114,41 +162,34 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
114
162
|
// Copy assets
|
|
115
163
|
const assetsSrcDir = path.join(__dirname, '..', 'assets');
|
|
116
164
|
const assetsDestDir = path.join(OUTPUT_DIR, 'assets');
|
|
117
|
-
|
|
165
|
+
|
|
118
166
|
if (await fs.pathExists(assetsSrcDir)) {
|
|
119
167
|
if (!options.isDev) {
|
|
120
168
|
console.log(`📂 Copying docmd assets to ${formatPathForDisplay(assetsDestDir, CWD)}...`);
|
|
121
169
|
}
|
|
122
|
-
|
|
170
|
+
|
|
123
171
|
// Create destination directory if it doesn't exist
|
|
124
172
|
await fs.ensureDir(assetsDestDir);
|
|
125
|
-
|
|
173
|
+
|
|
126
174
|
// Get all files from source directory recursively
|
|
127
175
|
const assetFiles = await getAllFiles(assetsSrcDir);
|
|
128
|
-
|
|
129
|
-
// Copy each file individually, checking for existing files if preserve flag is set
|
|
130
176
|
for (const srcFile of assetFiles) {
|
|
131
177
|
const relativePath = path.relative(assetsSrcDir, srcFile);
|
|
132
178
|
const destFile = path.join(assetsDestDir, relativePath);
|
|
133
|
-
|
|
134
|
-
// Check if destination file already exists
|
|
135
179
|
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)
|
|
180
|
+
|
|
140
181
|
if (fileExists && (options.preserve || userAssetsCopied.includes(relativePath))) {
|
|
141
|
-
// Skip file and add to preserved list
|
|
142
182
|
preservedFiles.push(relativePath);
|
|
143
183
|
if (!options.isDev && options.preserve) {
|
|
144
184
|
console.log(` Preserving existing file: ${relativePath}`);
|
|
145
185
|
}
|
|
186
|
+
|
|
146
187
|
} else {
|
|
147
|
-
// Copy file (either it doesn't exist or we're not preserving)
|
|
148
188
|
await fs.ensureDir(path.dirname(destFile));
|
|
149
|
-
await
|
|
189
|
+
await processAndCopyAsset(srcFile, destFile);
|
|
150
190
|
}
|
|
151
191
|
}
|
|
192
|
+
|
|
152
193
|
} else {
|
|
153
194
|
console.warn(`⚠️ Assets source directory not found: ${formatPathForDisplay(assetsSrcDir, CWD)}`);
|
|
154
195
|
}
|
|
@@ -189,7 +230,7 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
189
230
|
for (const filePath of markdownFiles) {
|
|
190
231
|
try {
|
|
191
232
|
const relativePath = path.relative(SRC_DIR, filePath);
|
|
192
|
-
|
|
233
|
+
|
|
193
234
|
// Skip file if already processed in this dev build cycle
|
|
194
235
|
if (options.noDoubleProcessing && processedFiles.has(relativePath)) {
|
|
195
236
|
continue;
|
|
@@ -203,12 +244,12 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
203
244
|
if (!processedData) {
|
|
204
245
|
continue;
|
|
205
246
|
}
|
|
206
|
-
|
|
247
|
+
|
|
207
248
|
// Destructure the valid data
|
|
208
|
-
const { frontmatter: pageFrontmatter, htmlContent, headings } = processedData;
|
|
209
|
-
|
|
249
|
+
const { frontmatter: pageFrontmatter, htmlContent, headings, searchData } = processedData;
|
|
250
|
+
|
|
210
251
|
const isIndexFile = path.basename(relativePath) === 'index.md';
|
|
211
|
-
|
|
252
|
+
|
|
212
253
|
let outputHtmlPath;
|
|
213
254
|
if (isIndexFile) {
|
|
214
255
|
const dirPath = path.dirname(relativePath);
|
|
@@ -221,9 +262,9 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
221
262
|
|
|
222
263
|
let relativePathToRoot = path.relative(path.dirname(finalOutputHtmlPath), OUTPUT_DIR);
|
|
223
264
|
if (relativePathToRoot === '') {
|
|
224
|
-
|
|
265
|
+
relativePathToRoot = './';
|
|
225
266
|
} else {
|
|
226
|
-
|
|
267
|
+
relativePathToRoot = relativePathToRoot.replace(/\\/g, '/') + '/';
|
|
227
268
|
}
|
|
228
269
|
|
|
229
270
|
let normalizedPath = path.relative(SRC_DIR, filePath).replace(/\\/g, '/');
|
|
@@ -259,7 +300,7 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
259
300
|
const cleanPath = prevPage.path.substring(1);
|
|
260
301
|
prevPage.url = relativePathToRoot + cleanPath;
|
|
261
302
|
}
|
|
262
|
-
|
|
303
|
+
|
|
263
304
|
if (nextPage) {
|
|
264
305
|
const cleanPath = nextPage.path.substring(1);
|
|
265
306
|
nextPage.url = relativePathToRoot + cleanPath;
|
|
@@ -286,17 +327,35 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
286
327
|
await fs.ensureDir(path.dirname(finalOutputHtmlPath));
|
|
287
328
|
await fs.writeFile(finalOutputHtmlPath, pageHtml);
|
|
288
329
|
|
|
289
|
-
const sitemapOutputPath = isIndexFile
|
|
290
|
-
|
|
291
|
-
|
|
330
|
+
const sitemapOutputPath = isIndexFile
|
|
331
|
+
? (path.dirname(relativePath) === '.' ? '' : path.dirname(relativePath) + '/')
|
|
332
|
+
: relativePath.replace(/\.md$/, '/');
|
|
292
333
|
|
|
293
334
|
processedPages.push({
|
|
294
335
|
outputPath: sitemapOutputPath.replace(/\\/g, '/'),
|
|
295
336
|
frontmatter: pageFrontmatter
|
|
296
337
|
});
|
|
338
|
+
|
|
339
|
+
// Collect Search Data
|
|
340
|
+
if (searchData) {
|
|
341
|
+
let pageUrl = outputHtmlPath.replace(/\\/g, '/');
|
|
342
|
+
if (pageUrl.endsWith('/index.html')) {
|
|
343
|
+
pageUrl = pageUrl.substring(0, pageUrl.length - 10); // remove index.html
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Add to index array
|
|
347
|
+
searchIndexData.push({
|
|
348
|
+
id: pageUrl, // URL is the ID
|
|
349
|
+
title: searchData.title,
|
|
350
|
+
text: searchData.content,
|
|
351
|
+
headings: searchData.headings.join(' ')
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
297
355
|
} catch (error) {
|
|
298
356
|
console.error(`❌ An unexpected error occurred while processing file ${path.relative(CWD, filePath)}:`, error);
|
|
299
357
|
}
|
|
358
|
+
|
|
300
359
|
}
|
|
301
360
|
|
|
302
361
|
// Generate sitemap if enabled in config
|
|
@@ -308,13 +367,40 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
308
367
|
}
|
|
309
368
|
}
|
|
310
369
|
|
|
370
|
+
// Generate search index if enabled
|
|
371
|
+
if (config.search !== false) {
|
|
372
|
+
console.log('🔍 Generating search index...');
|
|
373
|
+
|
|
374
|
+
// Create MiniSearch instance
|
|
375
|
+
const miniSearch = new MiniSearch({
|
|
376
|
+
fields: ['title', 'headings', 'text'], // fields to index for full-text search
|
|
377
|
+
storeFields: ['title', 'id', 'text'], // fields to return with search results (don't store full text to keep JSON small)
|
|
378
|
+
searchOptions: {
|
|
379
|
+
boost: { title: 2, headings: 1.5 }, // title matches are more important
|
|
380
|
+
fuzzy: 0.2
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Add documents
|
|
385
|
+
miniSearch.addAll(searchIndexData);
|
|
386
|
+
|
|
387
|
+
// Serialize to JSON
|
|
388
|
+
const jsonIndex = JSON.stringify(miniSearch.toJSON());
|
|
389
|
+
const searchIndexPath = path.join(OUTPUT_DIR, 'search-index.json');
|
|
390
|
+
await fs.writeFile(searchIndexPath, jsonIndex);
|
|
391
|
+
|
|
392
|
+
if (!options.isDev) {
|
|
393
|
+
console.log(`✅ Search index generated (${(jsonIndex.length / 1024).toFixed(1)} KB)`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
311
397
|
// Print summary of preserved files at the end of build
|
|
312
398
|
if (preservedFiles.length > 0 && !options.isDev) {
|
|
313
399
|
console.log(`\n📋 Build Summary: ${preservedFiles.length} existing files were preserved:`);
|
|
314
400
|
preservedFiles.forEach(file => console.log(` - assets/${file}`));
|
|
315
401
|
console.log(`\nTo update these files in future builds, run without the --preserve flag.`);
|
|
316
402
|
}
|
|
317
|
-
|
|
403
|
+
|
|
318
404
|
if (userAssetsCopied.length > 0 && !options.isDev) {
|
|
319
405
|
console.log(`\n📋 User Assets: ${userAssetsCopied.length} files were copied from your assets/ directory:`);
|
|
320
406
|
if (userAssetsCopied.length <= 10) {
|
|
@@ -325,6 +411,74 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
325
411
|
}
|
|
326
412
|
}
|
|
327
413
|
|
|
414
|
+
// Bundle third-party libraries into assets
|
|
415
|
+
const copyLibrary = async (packageName, fileToBundle, destFileName) => {
|
|
416
|
+
try {
|
|
417
|
+
let srcPath;
|
|
418
|
+
|
|
419
|
+
// 1. Resolve Source Path
|
|
420
|
+
try {
|
|
421
|
+
srcPath = require.resolve(`${packageName}/${fileToBundle}`);
|
|
422
|
+
} catch (e) {
|
|
423
|
+
const mainPath = require.resolve(packageName);
|
|
424
|
+
let currentDir = path.dirname(mainPath);
|
|
425
|
+
let packageRoot = null;
|
|
426
|
+
|
|
427
|
+
for (let i = 0; i < 5; i++) {
|
|
428
|
+
if (await fs.pathExists(path.join(currentDir, 'package.json'))) {
|
|
429
|
+
packageRoot = currentDir;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
currentDir = path.dirname(currentDir);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (packageRoot) {
|
|
436
|
+
srcPath = path.join(packageRoot, fileToBundle);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// 2. Process and Write
|
|
441
|
+
if (srcPath && await fs.pathExists(srcPath)) {
|
|
442
|
+
const destPath = path.join(OUTPUT_DIR, 'assets/js', destFileName);
|
|
443
|
+
|
|
444
|
+
// Read content
|
|
445
|
+
let content = await fs.readFile(srcPath, 'utf8');
|
|
446
|
+
|
|
447
|
+
// This prevents the browser from looking for index.js.map or similar files we didn't copy
|
|
448
|
+
content = content.replace(/\/\/# sourceMappingURL=.*$/gm, '');
|
|
449
|
+
|
|
450
|
+
// Minify if production build
|
|
451
|
+
if (shouldMinify) {
|
|
452
|
+
try {
|
|
453
|
+
const result = await esbuild.transform(content, {
|
|
454
|
+
minify: true,
|
|
455
|
+
loader: 'js',
|
|
456
|
+
target: 'es2015'
|
|
457
|
+
});
|
|
458
|
+
await fs.writeFile(destPath, result.code);
|
|
459
|
+
} catch (minErr) {
|
|
460
|
+
console.warn(`⚠️ Minification failed for ${packageName}, using sanitized original.`, minErr.message);
|
|
461
|
+
await fs.writeFile(destPath, content);
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
// Write sanitized original in dev mode
|
|
465
|
+
await fs.writeFile(destPath, content);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
} else {
|
|
469
|
+
console.warn(`⚠️ Could not locate ${fileToBundle} in ${packageName}`);
|
|
470
|
+
}
|
|
471
|
+
} catch (e) {
|
|
472
|
+
console.warn(`⚠️ Failed to bundle ${packageName}: ${e.message}`);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// Bundle MiniSearch
|
|
477
|
+
await copyLibrary('minisearch', 'dist/umd/index.js', 'minisearch.js');
|
|
478
|
+
|
|
479
|
+
// Bundle Mermaid
|
|
480
|
+
await copyLibrary('mermaid', 'dist/mermaid.min.js', 'mermaid.min.js');
|
|
481
|
+
|
|
328
482
|
return {
|
|
329
483
|
config,
|
|
330
484
|
processedPages,
|
|
@@ -336,10 +490,10 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
336
490
|
async function findFilesToCleanup(dir) {
|
|
337
491
|
const filesToRemove = [];
|
|
338
492
|
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
339
|
-
|
|
493
|
+
|
|
340
494
|
for (const item of items) {
|
|
341
495
|
const fullPath = path.join(dir, item.name);
|
|
342
|
-
|
|
496
|
+
|
|
343
497
|
if (item.isDirectory()) {
|
|
344
498
|
// Don't delete the assets directory
|
|
345
499
|
if (item.name !== 'assets') {
|
|
@@ -347,13 +501,13 @@ async function findFilesToCleanup(dir) {
|
|
|
347
501
|
filesToRemove.push(...subDirFiles);
|
|
348
502
|
}
|
|
349
503
|
} else if (
|
|
350
|
-
item.name.endsWith('.html') ||
|
|
504
|
+
item.name.endsWith('.html') ||
|
|
351
505
|
item.name === 'sitemap.xml'
|
|
352
506
|
) {
|
|
353
507
|
filesToRemove.push(fullPath);
|
|
354
508
|
}
|
|
355
509
|
}
|
|
356
|
-
|
|
510
|
+
|
|
357
511
|
return filesToRemove;
|
|
358
512
|
}
|
|
359
513
|
|
|
@@ -361,7 +515,7 @@ async function findFilesToCleanup(dir) {
|
|
|
361
515
|
async function getAllFiles(dir) {
|
|
362
516
|
const files = [];
|
|
363
517
|
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
364
|
-
|
|
518
|
+
|
|
365
519
|
for (const item of items) {
|
|
366
520
|
const fullPath = path.join(dir, item.name);
|
|
367
521
|
if (item.isDirectory()) {
|
|
@@ -370,7 +524,7 @@ async function getAllFiles(dir) {
|
|
|
370
524
|
files.push(fullPath);
|
|
371
525
|
}
|
|
372
526
|
}
|
|
373
|
-
|
|
527
|
+
|
|
374
528
|
return files;
|
|
375
529
|
}
|
|
376
530
|
|
package/src/commands/init.js
CHANGED
|
@@ -24,6 +24,12 @@ module.exports = {
|
|
|
24
24
|
srcDir: 'docs', // Source directory for Markdown files
|
|
25
25
|
outputDir: 'site', // Directory for generated static site
|
|
26
26
|
|
|
27
|
+
// Search Configuration
|
|
28
|
+
search: true, // Enable/disable search functionality
|
|
29
|
+
|
|
30
|
+
// Build Options
|
|
31
|
+
minify: true, // Enable/disable HTML/CSS/JS minification
|
|
32
|
+
|
|
27
33
|
// Sidebar Configuration
|
|
28
34
|
sidebar: {
|
|
29
35
|
collapsible: true, // or false to disable
|
|
@@ -88,6 +94,15 @@ module.exports = {
|
|
|
88
94
|
// Add other future plugin configurations here by their key
|
|
89
95
|
},
|
|
90
96
|
|
|
97
|
+
// "Edit this page" Link Configuration
|
|
98
|
+
editLink: {
|
|
99
|
+
enabled: false,
|
|
100
|
+
// The URL to the folder containing your docs in the git repo
|
|
101
|
+
// Note: It usually ends with /edit/main/docs or /blob/main/docs
|
|
102
|
+
baseUrl: 'https://github.com/mgks/docmd/edit/main/docs',
|
|
103
|
+
text: 'Edit this page on GitHub'
|
|
104
|
+
},
|
|
105
|
+
|
|
91
106
|
// Navigation Structure (Sidebar)
|
|
92
107
|
// Icons are kebab-case names from Lucide Icons (https://lucide.dev/)
|
|
93
108
|
navigation: [
|
|
@@ -4,6 +4,7 @@ const fs = require('fs-extra');
|
|
|
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');
|
|
7
8
|
|
|
8
9
|
function decodeHtmlEntities(html) {
|
|
9
10
|
return html.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ');
|
|
@@ -56,8 +57,18 @@ async function processMarkdownFile(filePath, md, config) {
|
|
|
56
57
|
htmlContent = md.render(markdownContent);
|
|
57
58
|
headings = extractHeadingsFromHtml(htmlContent);
|
|
58
59
|
}
|
|
60
|
+
|
|
61
|
+
let searchData = null;
|
|
62
|
+
if (!frontmatter.noindex) {
|
|
63
|
+
const rawText = decodeHtmlEntities(striptags(htmlContent));
|
|
64
|
+
searchData = {
|
|
65
|
+
title: frontmatter.title || 'Untitled',
|
|
66
|
+
content: rawText.slice(0, 5000), // Safety cap to prevent massive JSON
|
|
67
|
+
headings: headings.map(h => h.text)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
59
70
|
|
|
60
|
-
return { frontmatter, htmlContent, headings };
|
|
71
|
+
return { frontmatter, htmlContent, headings, searchData };
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
async function findMarkdownFiles(dir) {
|
|
@@ -100,6 +100,27 @@ async function generateHtmlPage(templateData) {
|
|
|
100
100
|
|
|
101
101
|
const isActivePage = currentPagePath && content && content.trim().length > 0;
|
|
102
102
|
|
|
103
|
+
// Calculate Edit Link
|
|
104
|
+
let editUrl = null;
|
|
105
|
+
let editLinkText = 'Edit this page';
|
|
106
|
+
|
|
107
|
+
if (config.editLink && config.editLink.enabled && config.editLink.baseUrl) {
|
|
108
|
+
// Normalize URL (remove trailing slash)
|
|
109
|
+
const baseUrl = config.editLink.baseUrl.replace(/\/$/, '');
|
|
110
|
+
|
|
111
|
+
// Get the source file path relative to srcDir
|
|
112
|
+
let relativeSourcePath = outputPath
|
|
113
|
+
.replace(/\/index\.html$/, '.md') // folder/index.html -> folder.md
|
|
114
|
+
.replace(/\\/g, '/'); // fix windows slashes
|
|
115
|
+
|
|
116
|
+
// Special case: The root index.html comes from index.md
|
|
117
|
+
if (relativeSourcePath === 'index.html') relativeSourcePath = 'index.md';
|
|
118
|
+
|
|
119
|
+
// Let's assume a standard 1:1 mapping for v0.2.x
|
|
120
|
+
editUrl = `${baseUrl}/${relativeSourcePath}`;
|
|
121
|
+
editLinkText = config.editLink.text || editLinkText;
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
const ejsData = {
|
|
104
125
|
content,
|
|
105
126
|
pageTitle,
|
|
@@ -107,6 +128,8 @@ async function generateHtmlPage(templateData) {
|
|
|
107
128
|
description: frontmatter.description,
|
|
108
129
|
siteTitle,
|
|
109
130
|
navigationHtml,
|
|
131
|
+
editUrl,
|
|
132
|
+
editLinkText,
|
|
110
133
|
defaultMode: config.theme?.defaultMode || 'light',
|
|
111
134
|
relativePathToRoot,
|
|
112
135
|
logo: config.logo,
|
|
@@ -17,20 +17,36 @@ const headingIdPlugin = (md) => {
|
|
|
17
17
|
const originalHeadingOpen = md.renderer.rules.heading_open || function(tokens, idx, options, env, self) {
|
|
18
18
|
return self.renderToken(tokens, idx, options);
|
|
19
19
|
};
|
|
20
|
+
|
|
20
21
|
md.renderer.rules.heading_open = function(tokens, idx, options, env, self) {
|
|
21
22
|
const token = tokens[idx];
|
|
22
|
-
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
|
|
24
|
+
// Check if an ID was already set by markdown-it-attrs (e.g. {#my-id})
|
|
25
|
+
const existingId = token.attrGet('id');
|
|
26
|
+
|
|
27
|
+
// If NO ID exists, generate one automatically from the text content
|
|
28
|
+
if (!existingId) {
|
|
29
|
+
const contentToken = tokens[idx + 1];
|
|
30
|
+
if (contentToken && contentToken.type === 'inline' && contentToken.content) {
|
|
31
|
+
const headingText = contentToken.content;
|
|
32
|
+
|
|
33
|
+
// Note: markdown-it-attrs strips the curly braces content from .content
|
|
34
|
+
// BEFORE this rule runs, so headingText should be clean.
|
|
35
|
+
|
|
36
|
+
const id = headingText
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/\s+/g, '-') // Replace spaces with -
|
|
39
|
+
.replace(/[^\w\u4e00-\u9fa5-]+/g, '') // Remove all non-word chars (keeping hyphens). Added unicode support implies keeping more chars if needed.
|
|
40
|
+
.replace(/--+/g, '-') // Replace multiple - with single -
|
|
41
|
+
.replace(/^-+/, '') // Trim - from start of text
|
|
42
|
+
.replace(/-+$/, ''); // Trim - from end of text
|
|
43
|
+
|
|
44
|
+
if (id) {
|
|
45
|
+
token.attrSet('id', id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
33
48
|
}
|
|
49
|
+
|
|
34
50
|
return originalHeadingOpen(tokens, idx, options, env, self);
|
|
35
51
|
};
|
|
36
52
|
};
|
package/src/templates/layout.ejs
CHANGED
|
@@ -66,6 +66,15 @@
|
|
|
66
66
|
</div>
|
|
67
67
|
<% if (theme && theme.enableModeToggle && theme.positionMode === 'top') { %>
|
|
68
68
|
<div class="header-right">
|
|
69
|
+
<% if (config.search !== false) { %>
|
|
70
|
+
<button class="docmd-search-trigger" aria-label="Search">
|
|
71
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><path d="m21 21-4.34-4.34"></path><circle cx="11" cy="11" r="8"></circle></svg>
|
|
72
|
+
<span class="search-label">Search</span>
|
|
73
|
+
<span class="search-keys">
|
|
74
|
+
<kbd class="docmd-kbd">⌘</kbd><kbd class="docmd-kbd">k</kbd>
|
|
75
|
+
</span>
|
|
76
|
+
</button>
|
|
77
|
+
<% } %>
|
|
69
78
|
<button id="theme-toggle-button" aria-label="Toggle theme" class="theme-toggle-button theme-toggle-header">
|
|
70
79
|
<%# renderIcon is available in the global EJS scope from html-generator %>
|
|
71
80
|
<%- renderIcon('sun', { class: 'icon-sun' }) %>
|
|
@@ -115,23 +124,66 @@
|
|
|
115
124
|
<%- include('toc', { content, headings, navigationHtml, isActivePage }) %>
|
|
116
125
|
</div>
|
|
117
126
|
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Page footer actions -->
|
|
129
|
+
<div class="page-footer-actions">
|
|
130
|
+
<% if (locals.editUrl) { %>
|
|
131
|
+
<a href="<%= editUrl %>" target="_blank" rel="noopener noreferrer" class="edit-link">
|
|
132
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pencil"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
|
|
133
|
+
<%= editLinkText %>
|
|
134
|
+
</a>
|
|
135
|
+
<% } %>
|
|
136
|
+
|
|
137
|
+
<% if (locals.lastUpdated) { %>
|
|
138
|
+
<!-- Placeholder for future Last Updated feature -->
|
|
139
|
+
<% } %>
|
|
140
|
+
</div>
|
|
118
141
|
</main>
|
|
142
|
+
|
|
119
143
|
<footer class="page-footer">
|
|
120
144
|
<div class="footer-content">
|
|
121
145
|
<div class="user-footer">
|
|
122
146
|
<%- footerHtml || '' %>
|
|
123
147
|
</div>
|
|
124
148
|
<div class="branding-footer">
|
|
125
|
-
Build with <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"></path><path d="M12 5 9.04 7.96a2.17 2.17 0 0 0 0 3.08c.82.82 2.13.85 3 .07l2.07-1.9a2.82 2.82 0 0 1 3.79 0l2.96 2.66"></path><path d="m18 15-2-2"></path><path d="m15 18-2-2"></path></svg> <a href="https://
|
|
149
|
+
Build with <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"></path><path d="M12 5 9.04 7.96a2.17 2.17 0 0 0 0 3.08c.82.82 2.13.85 3 .07l2.07-1.9a2.82 2.82 0 0 1 3.79 0l2.96 2.66"></path><path d="m18 15-2-2"></path><path d="m15 18-2-2"></path></svg> <a href="https://github.com/mgks/docmd" target="_blank" rel="noopener">docmd.</a>
|
|
126
150
|
</div>
|
|
127
151
|
</div>
|
|
128
152
|
</footer>
|
|
129
153
|
</div>
|
|
130
154
|
|
|
155
|
+
<% if (config.search !== false) { %>
|
|
156
|
+
<!-- Search Modal -->
|
|
157
|
+
<div id="docmd-search-modal" class="docmd-search-modal" style="display: none;">
|
|
158
|
+
<div class="docmd-search-box">
|
|
159
|
+
<div class="docmd-search-header">
|
|
160
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1.25em" height="1.25em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><path d="m21 21-4.34-4.34"></path><circle cx="11" cy="11" r="8"></circle></svg>
|
|
161
|
+
<input type="text" id="docmd-search-input" placeholder="Search documentation..." autocomplete="off" spellcheck="false">
|
|
162
|
+
<button onclick="window.closeDocmdSearch()" class="docmd-search-close" aria-label="Close search">
|
|
163
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
<div id="docmd-search-results" class="docmd-search-results">
|
|
167
|
+
<!-- Results injected here -->
|
|
168
|
+
</div>
|
|
169
|
+
<div class="docmd-search-footer">
|
|
170
|
+
<span><kbd class="docmd-kbd">↑</kbd> <kbd class="docmd-kbd">↓</kbd> to navigate</span>
|
|
171
|
+
<span><kbd class="docmd-kbd">ESC</kbd> to close</span>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<% } %>
|
|
176
|
+
|
|
131
177
|
<script src="<%= relativePathToRoot %>assets/js/docmd-main.js"></script>
|
|
132
|
-
|
|
178
|
+
|
|
179
|
+
<% if (config.search !== false) { %>
|
|
180
|
+
<!-- Search Scripts -->
|
|
181
|
+
<script src="<%= relativePathToRoot %>assets/js/minisearch.js"></script>
|
|
182
|
+
<script src="<%= relativePathToRoot %>assets/js/docmd-search.js"></script>
|
|
183
|
+
<% } %>
|
|
184
|
+
|
|
133
185
|
<!-- Mermaid.js for diagram rendering -->
|
|
134
|
-
<script src="
|
|
186
|
+
<script src="<%= relativePathToRoot %>assets/js/mermaid.min.js"></script>
|
|
135
187
|
<script src="<%= relativePathToRoot %>assets/js/docmd-mermaid.js"></script>
|
|
136
188
|
|
|
137
189
|
<% (customJsFiles || []).forEach(jsFile => { %>
|