@mgks/docmd 0.3.9 → 0.4.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.
Files changed (47) hide show
  1. package/README.md +15 -160
  2. package/bin/docmd.js +6 -69
  3. package/package.json +6 -79
  4. package/bin/postinstall.js +0 -14
  5. package/src/assets/css/docmd-highlight-dark.css +0 -86
  6. package/src/assets/css/docmd-highlight-light.css +0 -86
  7. package/src/assets/css/docmd-main.css +0 -1736
  8. package/src/assets/css/docmd-theme-retro.css +0 -867
  9. package/src/assets/css/docmd-theme-ruby.css +0 -629
  10. package/src/assets/css/docmd-theme-sky.css +0 -617
  11. package/src/assets/favicon.ico +0 -0
  12. package/src/assets/images/docmd-logo-dark.png +0 -0
  13. package/src/assets/images/docmd-logo-light.png +0 -0
  14. package/src/assets/js/docmd-image-lightbox.js +0 -74
  15. package/src/assets/js/docmd-main.js +0 -260
  16. package/src/assets/js/docmd-mermaid.js +0 -205
  17. package/src/assets/js/docmd-search.js +0 -218
  18. package/src/commands/build.js +0 -237
  19. package/src/commands/dev.js +0 -352
  20. package/src/commands/init.js +0 -277
  21. package/src/commands/live.js +0 -145
  22. package/src/core/asset-manager.js +0 -72
  23. package/src/core/config-loader.js +0 -58
  24. package/src/core/config-validator.js +0 -80
  25. package/src/core/file-processor.js +0 -103
  26. package/src/core/fs-utils.js +0 -40
  27. package/src/core/html-generator.js +0 -184
  28. package/src/core/icon-renderer.js +0 -106
  29. package/src/core/logger.js +0 -21
  30. package/src/core/markdown/containers.js +0 -94
  31. package/src/core/markdown/renderers.js +0 -90
  32. package/src/core/markdown/rules.js +0 -402
  33. package/src/core/markdown/setup.js +0 -113
  34. package/src/core/navigation-helper.js +0 -74
  35. package/src/index.js +0 -12
  36. package/src/live/core.js +0 -67
  37. package/src/live/index.html +0 -216
  38. package/src/live/live.css +0 -256
  39. package/src/live/shims.js +0 -1
  40. package/src/plugins/analytics.js +0 -48
  41. package/src/plugins/seo.js +0 -107
  42. package/src/plugins/sitemap.js +0 -127
  43. package/src/templates/layout.ejs +0 -187
  44. package/src/templates/navigation.ejs +0 -87
  45. package/src/templates/no-style.ejs +0 -166
  46. package/src/templates/partials/theme-init.js +0 -30
  47. package/src/templates/toc.ejs +0 -38
@@ -1,260 +0,0 @@
1
- // Source file from the docmd project — https://github.com/docmd-io/docmd
2
-
3
- /*
4
- * Main client-side script for docmd UI interactions
5
- */
6
-
7
- // --- Collapsible Navigation Logic ---
8
- function initializeCollapsibleNav() {
9
- const nav = document.querySelector('.sidebar-nav');
10
- if (!nav) return;
11
-
12
- // We NO LONGER set initial state here.
13
- // The HTML arrives with style="display: block" and aria-expanded="true"
14
- // pre-rendered by the build process. This eliminates the FOUC/Jitter.
15
-
16
- nav.querySelectorAll('li.collapsible').forEach(item => {
17
- const anchor = item.querySelector('a');
18
- const submenu = item.querySelector('.submenu');
19
-
20
- if (!anchor || !submenu) return;
21
-
22
- // Only handle CLICK events to toggle state
23
- anchor.addEventListener('click', (e) => {
24
- const href = anchor.getAttribute('href');
25
- // If it's a placeholder link (#) OR the user clicked the arrow icon
26
- const isToggleAction = !href || href === '#' || e.target.closest('.collapse-icon');
27
-
28
- if (isToggleAction) {
29
- e.preventDefault();
30
-
31
- // Toggle Logic
32
- const isExpanded = item.getAttribute('aria-expanded') === 'true';
33
- const newState = !isExpanded;
34
-
35
- item.setAttribute('aria-expanded', newState);
36
- submenu.style.display = newState ? 'block' : 'none';
37
- }
38
- });
39
- });
40
- }
41
-
42
- // --- Mobile Menu Logic ---
43
- function initializeMobileMenus() {
44
- const sidebarBtn = document.querySelector('.sidebar-menu-button');
45
- const sidebar = document.querySelector('.sidebar');
46
-
47
- if (sidebarBtn && sidebar) {
48
- sidebarBtn.addEventListener('click', (e) => {
49
- e.stopPropagation();
50
- sidebar.classList.toggle('mobile-expanded');
51
- });
52
- }
53
-
54
- const tocBtn = document.querySelector('.toc-menu-button');
55
- const tocContainer = document.querySelector('.toc-container');
56
- const tocTitle = document.querySelector('.toc-title');
57
-
58
- const toggleToc = (e) => {
59
- if (window.getComputedStyle(tocBtn).display === 'none') return;
60
- e.stopPropagation();
61
- tocContainer.classList.toggle('mobile-expanded');
62
- };
63
-
64
- if (tocBtn && tocContainer) {
65
- tocBtn.addEventListener('click', toggleToc);
66
- if (tocTitle) tocTitle.addEventListener('click', toggleToc);
67
- }
68
- }
69
-
70
- // --- Sidebar Scroll Preservation (Instant Center) ---
71
- function initializeSidebarScroll() {
72
- const sidebar = document.querySelector('.sidebar');
73
- if (!sidebar) return;
74
-
75
- // Wait for the layout to be stable
76
- requestAnimationFrame(() => {
77
- // Find the active link
78
- const activeElement = sidebar.querySelector('a.active');
79
-
80
- if (activeElement) {
81
- activeElement.scrollIntoView({
82
- behavior: 'auto', // INSTANT jump (prevents scrolling animation jitter)
83
- block: 'center', // Center it vertically in the sidebar
84
- inline: 'nearest'
85
- });
86
- }
87
- });
88
- }
89
-
90
- // --- Theme Toggle Logic ---
91
- function setupThemeToggleListener() {
92
- const themeToggleButton = document.getElementById('theme-toggle-button');
93
-
94
- function applyTheme(theme) {
95
- const validThemes = ['light', 'dark'];
96
- const selectedTheme = validThemes.includes(theme) ? theme : 'light';
97
-
98
- document.documentElement.setAttribute('data-theme', selectedTheme);
99
- document.body.setAttribute('data-theme', selectedTheme);
100
- localStorage.setItem('docmd-theme', selectedTheme);
101
-
102
- const highlightThemeLink = document.getElementById('highlight-theme');
103
- if (highlightThemeLink) {
104
- const baseHref = highlightThemeLink.getAttribute('data-base-href');
105
-
106
- if (baseHref) {
107
- const themeFile = `docmd-highlight-${selectedTheme}.css`;
108
- const cleanHref = baseHref + themeFile;
109
- highlightThemeLink.setAttribute('href', encodeURI(cleanHref));
110
- }
111
- }
112
- }
113
-
114
- if (themeToggleButton) {
115
- themeToggleButton.addEventListener('click', () => {
116
- const currentTheme = document.documentElement.getAttribute('data-theme');
117
- const newTheme = currentTheme === 'light' ? 'dark' : 'light';
118
- applyTheme(newTheme);
119
- });
120
- }
121
- }
122
-
123
- // --- Sidebar Collapse Logic ---
124
- function initializeSidebarToggle() {
125
- const toggleButton = document.getElementById('sidebar-toggle-button');
126
- const body = document.body;
127
-
128
- if (!body.classList.contains('sidebar-collapsible') || !toggleButton) return;
129
-
130
- const defaultConfigCollapsed = body.dataset.defaultCollapsed === 'true';
131
- let isCollapsed = localStorage.getItem('docmd-sidebar-collapsed');
132
-
133
- if (isCollapsed === null) isCollapsed = defaultConfigCollapsed;
134
- else isCollapsed = isCollapsed === 'true';
135
-
136
- if (isCollapsed) body.classList.add('sidebar-collapsed');
137
-
138
- toggleButton.addEventListener('click', () => {
139
- body.classList.toggle('sidebar-collapsed');
140
- const currentlyCollapsed = body.classList.contains('sidebar-collapsed');
141
- localStorage.setItem('docmd-sidebar-collapsed', currentlyCollapsed);
142
- });
143
- }
144
-
145
- // --- Tabs Container Logic ---
146
- function initializeTabs() {
147
- document.querySelectorAll('.docmd-tabs').forEach(tabsContainer => {
148
- const navItems = tabsContainer.querySelectorAll('.docmd-tabs-nav-item');
149
- const tabPanes = tabsContainer.querySelectorAll('.docmd-tab-pane');
150
-
151
- navItems.forEach((navItem, index) => {
152
- navItem.addEventListener('click', () => {
153
- navItems.forEach(item => item.classList.remove('active'));
154
- tabPanes.forEach(pane => pane.classList.remove('active'));
155
-
156
- navItem.classList.add('active');
157
- if (tabPanes[index]) tabPanes[index].classList.add('active');
158
- });
159
- });
160
- });
161
- }
162
-
163
- // --- Copy Code Button Logic ---
164
- function initializeCopyCodeButtons() {
165
- if (document.body.dataset.copyCodeEnabled !== 'true') return;
166
-
167
- const copyIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>`;
168
- const checkIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
169
-
170
- document.querySelectorAll('pre').forEach(preElement => {
171
- const codeElement = preElement.querySelector('code');
172
- if (!codeElement) return;
173
-
174
- const wrapper = document.createElement('div');
175
- wrapper.style.position = 'relative';
176
- wrapper.style.display = 'block';
177
-
178
- preElement.parentNode.insertBefore(wrapper, preElement);
179
- wrapper.appendChild(preElement);
180
- preElement.style.position = 'static';
181
-
182
- const copyButton = document.createElement('button');
183
- copyButton.className = 'copy-code-button';
184
- copyButton.innerHTML = copyIconSvg;
185
- copyButton.setAttribute('aria-label', 'Copy code to clipboard');
186
- wrapper.appendChild(copyButton);
187
-
188
- copyButton.addEventListener('click', () => {
189
- navigator.clipboard.writeText(codeElement.innerText).then(() => {
190
- copyButton.innerHTML = checkIconSvg;
191
- copyButton.classList.add('copied');
192
- setTimeout(() => {
193
- copyButton.innerHTML = copyIconSvg;
194
- copyButton.classList.remove('copied');
195
- }, 2000);
196
- }).catch(err => {
197
- console.error('Failed to copy text: ', err);
198
- copyButton.innerText = 'Error';
199
- });
200
- });
201
- });
202
- }
203
-
204
- // --- Theme Sync Function ---
205
- function syncBodyTheme() {
206
- const currentTheme = document.documentElement.getAttribute('data-theme');
207
- if (currentTheme && document.body) {
208
- document.body.setAttribute('data-theme', currentTheme);
209
- }
210
- }
211
-
212
- // --- Scroll Spy Logic ---
213
- function initializeScrollSpy() {
214
- const tocLinks = document.querySelectorAll('.toc-link');
215
- const headings = document.querySelectorAll('.main-content h2, .main-content h3');
216
-
217
- if (tocLinks.length === 0 || headings.length === 0) return;
218
-
219
- const observerOptions = {
220
- root: null,
221
- // Trigger when heading crosses the top 10% of screen
222
- rootMargin: '-10% 0px -80% 0px',
223
- threshold: 0
224
- };
225
-
226
- const observer = new IntersectionObserver((entries) => {
227
- entries.forEach(entry => {
228
- if (entry.isIntersecting) {
229
- // 1. Clear current active state
230
- tocLinks.forEach(link => link.classList.remove('active'));
231
-
232
- // 2. Find link corresponding to this heading
233
- const id = entry.target.getAttribute('id');
234
- const activeLink = document.querySelector(`.toc-link[href="#${id}"]`);
235
-
236
- if (activeLink) {
237
- activeLink.classList.add('active');
238
-
239
- // Optional: Auto-scroll the TOC sidebar itself if needed
240
- // activeLink.scrollIntoView({ block: 'nearest' });
241
- }
242
- }
243
- });
244
- }, observerOptions);
245
-
246
- headings.forEach(heading => observer.observe(heading));
247
- }
248
-
249
- // --- Main Execution ---
250
- document.addEventListener('DOMContentLoaded', () => {
251
- syncBodyTheme();
252
- setupThemeToggleListener();
253
- initializeSidebarToggle();
254
- initializeTabs();
255
- initializeCopyCodeButtons();
256
- initializeCollapsibleNav();
257
- initializeMobileMenus();
258
- initializeSidebarScroll();
259
- initializeScrollSpy();
260
- });
@@ -1,205 +0,0 @@
1
- // Source file from the docmd project — https://github.com/docmd-io/docmd
2
-
3
- /*
4
- * Mermaid diagram integration with theme support
5
- */
6
-
7
- (function () {
8
- 'use strict';
9
-
10
- // Configuration for mermaid based on current theme
11
- function getMermaidConfig(theme) {
12
- const isDark = theme === 'dark';
13
-
14
- return {
15
- startOnLoad: false,
16
- theme: isDark ? 'dark' : 'default',
17
- flowchart: {
18
- useMaxWidth: true,
19
- htmlLabels: true
20
- },
21
- sequence: {
22
- useMaxWidth: true
23
- },
24
- gantt: {
25
- useMaxWidth: true
26
- }
27
- };
28
- }
29
-
30
- // Initialize mermaid when DOM is ready
31
- function initializeMermaid() {
32
- if (typeof mermaid === 'undefined') {
33
- console.warn('Mermaid library not loaded');
34
- return;
35
- }
36
-
37
- const currentTheme = document.body.getAttribute('data-theme') || 'light';
38
- const config = getMermaidConfig(currentTheme);
39
-
40
- mermaid.initialize(config);
41
-
42
- // Render all mermaid diagrams
43
- renderMermaidDiagrams();
44
- }
45
-
46
- // Store for diagram codes
47
- const diagramStore = new Map();
48
-
49
- // Render all mermaid diagrams on the page
50
- function renderMermaidDiagrams() {
51
- if (typeof mermaid === 'undefined') {
52
- return;
53
- }
54
-
55
- const mermaidElements = document.querySelectorAll('pre.mermaid');
56
-
57
- mermaidElements.forEach((element, index) => {
58
- // Skip if already rendered
59
- if (element.getAttribute('data-processed') === 'true') {
60
- return;
61
- }
62
-
63
- try {
64
- // Get the diagram code
65
- const code = element.textContent;
66
-
67
- // Create a unique ID for this diagram
68
- const id = `mermaid-diagram-${index}-${Date.now()}`;
69
-
70
- // Store the original code for re-rendering
71
- diagramStore.set(id, code);
72
-
73
- // Create a container div
74
- const container = document.createElement('div');
75
- container.className = 'mermaid-container';
76
- container.setAttribute('data-mermaid-id', id);
77
- container.setAttribute('data-processed', 'true');
78
-
79
- // Replace the pre element with the container
80
- element.parentNode.replaceChild(container, element);
81
-
82
- // Render the diagram
83
- renderSingleDiagram(container, id, code);
84
- } catch (error) {
85
- console.error('Mermaid processing error:', error);
86
- }
87
- });
88
- }
89
-
90
- // Render a single diagram
91
- function renderSingleDiagram(container, id, code) {
92
- if (typeof mermaid === 'undefined') {
93
- return;
94
- }
95
-
96
- // Process the code to handle theme overrides
97
- const currentTheme = document.body.getAttribute('data-theme') || 'light';
98
- const processedCode = processThemeInCode(code, currentTheme);
99
-
100
- // Render the diagram
101
- mermaid.render(id, processedCode).then(result => {
102
- container.innerHTML = result.svg;
103
- }).catch(error => {
104
- console.error('Mermaid rendering error:', error);
105
- container.innerHTML = `<pre class="mermaid-error">Error rendering diagram: ${error.message}</pre>`;
106
- });
107
- }
108
-
109
- // Process mermaid code to inject or override theme
110
- function processThemeInCode(code, theme) {
111
- const isDark = theme === 'dark';
112
- const targetTheme = isDark ? 'dark' : 'default';
113
-
114
- // Check if code has %%{init: config - match the entire init block including nested objects
115
- const initRegex = /%%\{init:\s*\{.*?\}\s*\}%%/s;
116
- const match = code.match(initRegex);
117
-
118
- if (match) {
119
- // Code has init config, replace only the theme property
120
- const initBlock = match[0];
121
- let updatedBlock = initBlock;
122
-
123
- // Try to replace theme property
124
- if (initBlock.includes("'theme'")) {
125
- updatedBlock = initBlock.replace(/'theme'\s*:\s*'[^']*'/, `'theme':'${targetTheme}'`);
126
- } else if (initBlock.includes('"theme"')) {
127
- updatedBlock = initBlock.replace(/"theme"\s*:\s*"[^"]*"/, `"theme":"${targetTheme}"`);
128
- } else {
129
- // Add theme to the config - insert after the first {
130
- updatedBlock = initBlock.replace(/%%\{init:\s*\{/, `%%{init: {'theme':'${targetTheme}',`);
131
- }
132
-
133
- return code.replace(initRegex, updatedBlock);
134
- }
135
-
136
- // No init config, code will use global mermaid config
137
- return code;
138
- }
139
-
140
- // Re-render mermaid diagrams when theme changes
141
- function handleThemeChange() {
142
- if (typeof mermaid === 'undefined') {
143
- return;
144
- }
145
-
146
- const currentTheme = document.body.getAttribute('data-theme') || 'light';
147
- const config = getMermaidConfig(currentTheme);
148
-
149
- // Re-initialize mermaid with new theme
150
- mermaid.initialize(config);
151
-
152
- // Find all rendered diagrams and re-render them
153
- const containers = document.querySelectorAll('.mermaid-container[data-processed="true"]');
154
-
155
- containers.forEach((container) => {
156
- const mermaidId = container.getAttribute('data-mermaid-id');
157
- const code = diagramStore.get(mermaidId);
158
-
159
- if (code) {
160
- // Create a new unique ID for re-rendering
161
- const newId = `${mermaidId}-${Date.now()}`;
162
-
163
- // Clear the container and re-render
164
- container.innerHTML = '';
165
- renderSingleDiagram(container, newId, code);
166
- }
167
- });
168
- }
169
-
170
- // Setup theme change observer
171
- function setupThemeObserver() {
172
- const observer = new MutationObserver((mutations) => {
173
- mutations.forEach((mutation) => {
174
- if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
175
- handleThemeChange();
176
- }
177
- });
178
- });
179
-
180
- observer.observe(document.body, {
181
- attributes: true,
182
- attributeFilter: ['data-theme']
183
- });
184
- }
185
-
186
- // Initialize when DOM is ready
187
- if (document.readyState === 'loading') {
188
- document.addEventListener('DOMContentLoaded', function () {
189
- initializeMermaid();
190
- setupThemeObserver();
191
- });
192
- } else {
193
- initializeMermaid();
194
- setupThemeObserver();
195
- }
196
-
197
- // Handle tab switches - render mermaid in newly visible tabs
198
- document.addEventListener('click', function (e) {
199
- if (e.target.classList.contains('docmd-tabs-nav-item')) {
200
- // Wait a bit for tab content to be visible
201
- setTimeout(renderMermaidDiagrams, 100);
202
- }
203
- });
204
-
205
- })();
@@ -1,218 +0,0 @@
1
- // Source file from the docmd project — https://github.com/docmd-io/docmd
2
-
3
- /*
4
- * Client-side search functionality for docmd
5
- */
6
-
7
- (function() {
8
- let miniSearch = null;
9
- let isIndexLoaded = false;
10
- let selectedIndex = -1; // Track keyboard selection
11
-
12
- const searchModal = document.getElementById('docmd-search-modal');
13
- const searchInput = document.getElementById('docmd-search-input');
14
- const searchResults = document.getElementById('docmd-search-results');
15
-
16
- const ROOT_PATH = window.DOCMD_ROOT || './';
17
-
18
- if (!searchModal) return;
19
-
20
- const emptyStateHtml = '<div class="search-initial">Type to start searching...</div>';
21
-
22
- // 1. Open/Close Logic
23
- function openSearch() {
24
- searchModal.style.display = 'flex';
25
- searchInput.focus();
26
-
27
- if (!searchInput.value.trim()) {
28
- searchResults.innerHTML = emptyStateHtml;
29
- selectedIndex = -1;
30
- }
31
-
32
- if (!isIndexLoaded) loadIndex();
33
- }
34
-
35
- function closeSearch() {
36
- searchModal.style.display = 'none';
37
- selectedIndex = -1; // Reset selection
38
- }
39
-
40
- // 2. Keyboard Navigation & Shortcuts
41
- document.addEventListener('keydown', (e) => {
42
- // Open: Cmd+K / Ctrl+K
43
- if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
44
- e.preventDefault();
45
- if (searchModal.style.display === 'flex') {
46
- closeSearch();
47
- } else {
48
- openSearch();
49
- }
50
- }
51
-
52
- // Context: Only handle these if search is open
53
- if (searchModal.style.display === 'flex') {
54
- const items = searchResults.querySelectorAll('.search-result-item');
55
-
56
- if (e.key === 'Escape') {
57
- e.preventDefault();
58
- closeSearch();
59
- }
60
- else if (e.key === 'ArrowDown') {
61
- e.preventDefault();
62
- if (items.length === 0) return;
63
- selectedIndex = (selectedIndex + 1) % items.length;
64
- updateSelection(items);
65
- }
66
- else if (e.key === 'ArrowUp') {
67
- e.preventDefault();
68
- if (items.length === 0) return;
69
- selectedIndex = (selectedIndex - 1 + items.length) % items.length;
70
- updateSelection(items);
71
- }
72
- else if (e.key === 'Enter') {
73
- e.preventDefault();
74
- if (selectedIndex >= 0 && items[selectedIndex]) {
75
- items[selectedIndex].click();
76
- } else if (items.length > 0) {
77
- // If nothing selected but results exist, click the first one on Enter
78
- items[0].click();
79
- }
80
- }
81
- }
82
- });
83
-
84
- function updateSelection(items) {
85
- items.forEach((item, index) => {
86
- if (index === selectedIndex) {
87
- item.classList.add('selected');
88
- item.scrollIntoView({ block: 'nearest' });
89
- } else {
90
- item.classList.remove('selected');
91
- }
92
- });
93
- }
94
-
95
- // Click handlers
96
- document.querySelectorAll('.docmd-search-trigger').forEach(btn => {
97
- btn.addEventListener('click', openSearch);
98
- });
99
-
100
- searchModal.addEventListener('click', (e) => {
101
- if (e.target === searchModal) closeSearch();
102
- });
103
-
104
- // 3. Index Loading Logic
105
- async function loadIndex() {
106
- try {
107
-
108
- const indexUrl = `${ROOT_PATH}search-index.json`;
109
- const response = await fetch(indexUrl);
110
- if (!response.ok) throw new Error(response.status);
111
-
112
- const jsonString = await response.text();
113
-
114
- miniSearch = MiniSearch.loadJSON(jsonString, {
115
- fields: ['title', 'headings', 'text'],
116
- storeFields: ['title', 'id', 'text'],
117
- searchOptions: {
118
- fuzzy: 0.2,
119
- prefix: true,
120
- boost: { title: 2, headings: 1.5 }
121
- }
122
- });
123
-
124
- isIndexLoaded = true;
125
- // console.log('Search index loaded');
126
-
127
- if (searchInput.value.trim()) {
128
- searchInput.dispatchEvent(new Event('input'));
129
- }
130
-
131
- } catch (e) {
132
- console.error('Failed to load search index', e);
133
- searchResults.innerHTML = '<div class="search-error">Failed to load search index.</div>';
134
- }
135
- }
136
-
137
- // Helper: Snippets (Same as before)
138
- function getSnippet(text, query) {
139
- if (!text) return '';
140
- const terms = query.split(/\s+/).filter(t => t.length > 2);
141
- const lowerText = text.toLowerCase();
142
- let bestIndex = -1;
143
-
144
- for (const term of terms) {
145
- const idx = lowerText.indexOf(term.toLowerCase());
146
- if (idx >= 0) { bestIndex = idx; break; }
147
- }
148
-
149
- const contextSize = 60;
150
- let start = 0;
151
- let end = 120;
152
-
153
- if (bestIndex >= 0) {
154
- start = Math.max(0, bestIndex - contextSize);
155
- end = Math.min(text.length, bestIndex + contextSize);
156
- }
157
-
158
- let snippet = text.substring(start, end);
159
- if (start > 0) snippet = '...' + snippet;
160
- if (end < text.length) snippet = snippet + '...';
161
-
162
- const safeTerms = terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
163
- if (safeTerms) {
164
- const highlightRegex = new RegExp(`(${safeTerms})`, 'gi');
165
- snippet = snippet.replace(highlightRegex, '<mark>$1</mark>');
166
- }
167
- return snippet;
168
- }
169
-
170
- // 4. Search Execution
171
- searchInput.addEventListener('input', (e) => {
172
- const query = e.target.value.trim();
173
- selectedIndex = -1; // Reset selection on new input
174
-
175
- if (!query) {
176
- searchResults.innerHTML = emptyStateHtml;
177
- return;
178
- }
179
-
180
- if (!isIndexLoaded) return;
181
-
182
- const results = miniSearch.search(query);
183
- renderResults(results, query);
184
- });
185
-
186
- function renderResults(results, query) {
187
- if (results.length === 0) {
188
- searchResults.innerHTML = '<div class="search-no-results">No results found.</div>';
189
- return;
190
- }
191
-
192
- const html = results.slice(0, 10).map((result, index) => {
193
- const snippet = getSnippet(result.text, query);
194
-
195
- const linkHref = `${ROOT_PATH}${result.id}`;
196
-
197
- // Add data-index for mouse interaction tracking if needed
198
- return `
199
- <a href="${linkHref}" class="search-result-item" data-index="${index}" onclick="window.closeDocmdSearch()">
200
- <div class="search-result-title">${result.title}</div>
201
- <div class="search-result-preview">${snippet}</div>
202
- </a>
203
- `;
204
- }).join('');
205
-
206
- searchResults.innerHTML = html;
207
-
208
- // Optional: Allow mouse hover to update selectedIndex
209
- searchResults.querySelectorAll('.search-result-item').forEach((item, idx) => {
210
- item.addEventListener('mouseenter', () => {
211
- selectedIndex = idx;
212
- updateSelection(searchResults.querySelectorAll('.search-result-item'));
213
- });
214
- });
215
- }
216
-
217
- window.closeDocmdSearch = closeSearch;
218
- })();