@mgks/docmd 0.2.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ const MarkdownIt = require('markdown-it');
14
14
  const hljs = require('highlight.js');
15
15
  const CleanCSS = require('clean-css');
16
16
  const esbuild = require('esbuild');
17
+ const MiniSearch = require('minisearch');
17
18
 
18
19
  // Debug function to log navigation information
19
20
  function logNavigationPaths(pagePath, navPath, normalizedPath) {
@@ -64,6 +65,7 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
64
65
  const USER_ASSETS_DIR = path.resolve(CWD, 'assets');
65
66
  const md = createMarkdownItInstance(config);
66
67
  const shouldMinify = !options.isDev && config.minify !== false;
68
+ const searchIndexData = [];
67
69
 
68
70
  if (!await fs.pathExists(SRC_DIR)) {
69
71
  throw new Error(`Source directory not found: ${formatPathForDisplay(SRC_DIR, CWD)}`);
@@ -244,7 +246,7 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
244
246
  }
245
247
 
246
248
  // Destructure the valid data
247
- const { frontmatter: pageFrontmatter, htmlContent, headings } = processedData;
249
+ const { frontmatter: pageFrontmatter, htmlContent, headings, searchData } = processedData;
248
250
 
249
251
  const isIndexFile = path.basename(relativePath) === 'index.md';
250
252
 
@@ -333,9 +335,27 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
333
335
  outputPath: sitemapOutputPath.replace(/\\/g, '/'),
334
336
  frontmatter: pageFrontmatter
335
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
+
336
355
  } catch (error) {
337
356
  console.error(`❌ An unexpected error occurred while processing file ${path.relative(CWD, filePath)}:`, error);
338
357
  }
358
+
339
359
  }
340
360
 
341
361
  // Generate sitemap if enabled in config
@@ -347,6 +367,33 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
347
367
  }
348
368
  }
349
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
+
350
397
  // Print summary of preserved files at the end of build
351
398
  if (preservedFiles.length > 0 && !options.isDev) {
352
399
  console.log(`\n📋 Build Summary: ${preservedFiles.length} existing files were preserved:`);
@@ -364,6 +411,74 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
364
411
  }
365
412
  }
366
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
+
367
482
  return {
368
483
  config,
369
484
  processedPages,
@@ -23,6 +23,11 @@ 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
+
27
+ // Search Configuration
28
+ search: true, // Enable/disable search functionality
29
+
30
+ // Build Options
26
31
  minify: true, // Enable/disable HTML/CSS/JS minification
27
32
 
28
33
  // Sidebar Configuration
@@ -6,7 +6,8 @@ const chalk = require('chalk');
6
6
  const KNOWN_KEYS = [
7
7
  'siteTitle', 'siteUrl', 'srcDir', 'outputDir', 'logo',
8
8
  'sidebar', 'theme', 'customJs', 'autoTitleFromH1',
9
- 'copyCode', 'plugins', 'navigation', 'footer', 'sponsor', 'favicon'
9
+ 'copyCode', 'plugins', 'navigation', 'footer', 'sponsor', 'favicon',
10
+ 'search', 'minify', 'editLink', 'pageNavigation'
10
11
  ];
11
12
 
12
13
  // Common typos mapping
@@ -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) {
@@ -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' }) %>
@@ -137,16 +146,48 @@
137
146
  <%- footerHtml || '' %>
138
147
  </div>
139
148
  <div class="branding-footer">
140
- 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://docmd.mgks.dev" target="_blank" rel="noopener">docmd.</a>
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>
141
150
  </div>
142
151
  </div>
143
152
  </footer>
144
153
  </div>
145
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
+
177
+ <script>
178
+ window.DOCMD_ROOT = "<%= relativePathToRoot %>";
179
+ </script>
180
+
146
181
  <script src="<%= relativePathToRoot %>assets/js/docmd-main.js"></script>
147
-
182
+
183
+ <% if (config.search !== false) { %>
184
+ <!-- Search Scripts -->
185
+ <script src="<%= relativePathToRoot %>assets/js/minisearch.js"></script>
186
+ <script src="<%= relativePathToRoot %>assets/js/docmd-search.js"></script>
187
+ <% } %>
188
+
148
189
  <!-- Mermaid.js for diagram rendering -->
149
- <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
190
+ <script src="<%= relativePathToRoot %>assets/js/mermaid.min.js"></script>
150
191
  <script src="<%= relativePathToRoot %>assets/js/docmd-mermaid.js"></script>
151
192
 
152
193
  <% (customJsFiles || []).forEach(jsFile => { %>