@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.
@@ -3,6 +3,7 @@
3
3
  /*
4
4
  * Main client-side script for docmd UI interactions
5
5
  */
6
+
6
7
  // --- Collapsible Navigation Logic ---
7
8
  function initializeCollapsibleNav() {
8
9
  const nav = document.querySelector('.sidebar-nav');
@@ -20,7 +21,7 @@ function initializeCollapsibleNav() {
20
21
  const submenu = item.querySelector('.submenu');
21
22
 
22
23
  if (!navId || !anchor || !submenu) return;
23
-
24
+
24
25
  const isParentActive = item.classList.contains('active-parent');
25
26
  // Default to expanded if it's a parent of the active page, otherwise check stored state.
26
27
  let isExpanded = isParentActive || (navStates[navId] === true);
@@ -51,19 +52,19 @@ function initializeCollapsibleNav() {
51
52
  }
52
53
  });
53
54
 
54
- /* anchor.addEventListener('click', (e) => {
55
- // If the click target is the icon, ALWAYS prevent navigation and toggle.
56
- if (e.target.closest('.collapse-icon')) {
57
- e.preventDefault();
58
- toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
59
- }
60
- // If the link is just a placeholder, also prevent navigation and toggle.
61
- else if (anchor.getAttribute('href') === '#') {
62
- e.preventDefault();
63
- toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
64
- }
65
- // Otherwise, let the click proceed to navigate to the link.
66
- });*/
55
+ /* anchor.addEventListener('click', (e) => {
56
+ // If the click target is the icon, ALWAYS prevent navigation and toggle.
57
+ if (e.target.closest('.collapse-icon')) {
58
+ e.preventDefault();
59
+ toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
60
+ }
61
+ // If the link is just a placeholder, also prevent navigation and toggle.
62
+ else if (anchor.getAttribute('href') === '#') {
63
+ e.preventDefault();
64
+ toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
65
+ }
66
+ // Otherwise, let the click proceed to navigate to the link.
67
+ });*/
67
68
  });
68
69
  }
69
70
 
@@ -101,7 +102,7 @@ function setupThemeToggleListener() {
101
102
  document.documentElement.setAttribute('data-theme', theme);
102
103
  document.body.setAttribute('data-theme', theme);
103
104
  localStorage.setItem('docmd-theme', theme);
104
-
105
+
105
106
  // Switch highlight.js theme
106
107
  const highlightThemeLink = document.getElementById('highlight-theme');
107
108
  if (highlightThemeLink) {
@@ -131,7 +132,7 @@ function initializeSidebarToggle() {
131
132
 
132
133
  const defaultConfigCollapsed = body.dataset.defaultCollapsed === 'true';
133
134
  let isCollapsed = localStorage.getItem('docmd-sidebar-collapsed');
134
-
135
+
135
136
  if (isCollapsed === null) {
136
137
  isCollapsed = defaultConfigCollapsed;
137
138
  } else {
@@ -161,8 +162,8 @@ function initializeTabs() {
161
162
  tabPanes.forEach(pane => pane.classList.remove('active'));
162
163
 
163
164
  navItem.classList.add('active');
164
- if(tabPanes[index]) {
165
- tabPanes[index].classList.add('active');
165
+ if (tabPanes[index]) {
166
+ tabPanes[index].classList.add('active');
166
167
  }
167
168
  });
168
169
  });
@@ -186,16 +187,16 @@ function initializeCopyCodeButtons() {
186
187
  const wrapper = document.createElement('div');
187
188
  wrapper.style.position = 'relative';
188
189
  wrapper.style.display = 'block';
189
-
190
+
190
191
  // Insert the wrapper before the pre element
191
192
  preElement.parentNode.insertBefore(wrapper, preElement);
192
-
193
+
193
194
  // Move the pre element into the wrapper
194
195
  wrapper.appendChild(preElement);
195
-
196
+
196
197
  // Remove the relative positioning from pre since wrapper handles it
197
198
  preElement.style.position = 'static';
198
-
199
+
199
200
  const copyButton = document.createElement('button');
200
201
  copyButton.className = 'copy-code-button';
201
202
  copyButton.innerHTML = copyIconSvg;
@@ -224,7 +225,7 @@ function syncBodyTheme() {
224
225
  if (currentTheme && document.body) {
225
226
  document.body.setAttribute('data-theme', currentTheme);
226
227
  }
227
-
228
+
228
229
  // Also ensure highlight CSS matches the current theme
229
230
  const highlightThemeLink = document.getElementById('highlight-theme');
230
231
  if (highlightThemeLink && currentTheme) {
@@ -1,13 +1,16 @@
1
1
  // Source file from the docmd project — https://github.com/mgks/docmd
2
- // Mermaid diagram integration with theme support
3
2
 
4
- (function() {
3
+ /*
4
+ * Mermaid diagram integration with theme support
5
+ */
6
+
7
+ (function () {
5
8
  'use strict';
6
9
 
7
10
  // Configuration for mermaid based on current theme
8
11
  function getMermaidConfig(theme) {
9
12
  const isDark = theme === 'dark';
10
-
13
+
11
14
  return {
12
15
  startOnLoad: false,
13
16
  theme: isDark ? 'dark' : 'default',
@@ -33,16 +36,16 @@
33
36
 
34
37
  const currentTheme = document.body.getAttribute('data-theme') || 'light';
35
38
  const config = getMermaidConfig(currentTheme);
36
-
39
+
37
40
  mermaid.initialize(config);
38
-
41
+
39
42
  // Render all mermaid diagrams
40
43
  renderMermaidDiagrams();
41
44
  }
42
45
 
43
46
  // Store for diagram codes
44
47
  const diagramStore = new Map();
45
-
48
+
46
49
  // Render all mermaid diagrams on the page
47
50
  function renderMermaidDiagrams() {
48
51
  if (typeof mermaid === 'undefined') {
@@ -50,7 +53,7 @@
50
53
  }
51
54
 
52
55
  const mermaidElements = document.querySelectorAll('pre.mermaid');
53
-
56
+
54
57
  mermaidElements.forEach((element, index) => {
55
58
  // Skip if already rendered
56
59
  if (element.getAttribute('data-processed') === 'true') {
@@ -60,22 +63,22 @@
60
63
  try {
61
64
  // Get the diagram code
62
65
  const code = element.textContent;
63
-
66
+
64
67
  // Create a unique ID for this diagram
65
68
  const id = `mermaid-diagram-${index}-${Date.now()}`;
66
-
69
+
67
70
  // Store the original code for re-rendering
68
71
  diagramStore.set(id, code);
69
-
72
+
70
73
  // Create a container div
71
74
  const container = document.createElement('div');
72
75
  container.className = 'mermaid-container';
73
76
  container.setAttribute('data-mermaid-id', id);
74
77
  container.setAttribute('data-processed', 'true');
75
-
78
+
76
79
  // Replace the pre element with the container
77
80
  element.parentNode.replaceChild(container, element);
78
-
81
+
79
82
  // Render the diagram
80
83
  renderSingleDiagram(container, id, code);
81
84
  } catch (error) {
@@ -83,17 +86,17 @@
83
86
  }
84
87
  });
85
88
  }
86
-
89
+
87
90
  // Render a single diagram
88
91
  function renderSingleDiagram(container, id, code) {
89
92
  if (typeof mermaid === 'undefined') {
90
93
  return;
91
94
  }
92
-
95
+
93
96
  // Process the code to handle theme overrides
94
97
  const currentTheme = document.body.getAttribute('data-theme') || 'light';
95
98
  const processedCode = processThemeInCode(code, currentTheme);
96
-
99
+
97
100
  // Render the diagram
98
101
  mermaid.render(id, processedCode).then(result => {
99
102
  container.innerHTML = result.svg;
@@ -102,21 +105,21 @@
102
105
  container.innerHTML = `<pre class="mermaid-error">Error rendering diagram: ${error.message}</pre>`;
103
106
  });
104
107
  }
105
-
108
+
106
109
  // Process mermaid code to inject or override theme
107
110
  function processThemeInCode(code, theme) {
108
111
  const isDark = theme === 'dark';
109
112
  const targetTheme = isDark ? 'dark' : 'default';
110
-
113
+
111
114
  // Check if code has %%{init: config - match the entire init block including nested objects
112
115
  const initRegex = /%%\{init:\s*\{.*?\}\s*\}%%/s;
113
116
  const match = code.match(initRegex);
114
-
117
+
115
118
  if (match) {
116
119
  // Code has init config, replace only the theme property
117
120
  const initBlock = match[0];
118
121
  let updatedBlock = initBlock;
119
-
122
+
120
123
  // Try to replace theme property
121
124
  if (initBlock.includes("'theme'")) {
122
125
  updatedBlock = initBlock.replace(/'theme'\s*:\s*'[^']*'/, `'theme':'${targetTheme}'`);
@@ -126,10 +129,10 @@
126
129
  // Add theme to the config - insert after the first {
127
130
  updatedBlock = initBlock.replace(/%%\{init:\s*\{/, `%%{init: {'theme':'${targetTheme}',`);
128
131
  }
129
-
132
+
130
133
  return code.replace(initRegex, updatedBlock);
131
134
  }
132
-
135
+
133
136
  // No init config, code will use global mermaid config
134
137
  return code;
135
138
  }
@@ -142,21 +145,21 @@
142
145
 
143
146
  const currentTheme = document.body.getAttribute('data-theme') || 'light';
144
147
  const config = getMermaidConfig(currentTheme);
145
-
148
+
146
149
  // Re-initialize mermaid with new theme
147
150
  mermaid.initialize(config);
148
-
151
+
149
152
  // Find all rendered diagrams and re-render them
150
153
  const containers = document.querySelectorAll('.mermaid-container[data-processed="true"]');
151
-
154
+
152
155
  containers.forEach((container) => {
153
156
  const mermaidId = container.getAttribute('data-mermaid-id');
154
157
  const code = diagramStore.get(mermaidId);
155
-
158
+
156
159
  if (code) {
157
160
  // Create a new unique ID for re-rendering
158
161
  const newId = `${mermaidId}-${Date.now()}`;
159
-
162
+
160
163
  // Clear the container and re-render
161
164
  container.innerHTML = '';
162
165
  renderSingleDiagram(container, newId, code);
@@ -182,7 +185,7 @@
182
185
 
183
186
  // Initialize when DOM is ready
184
187
  if (document.readyState === 'loading') {
185
- document.addEventListener('DOMContentLoaded', function() {
188
+ document.addEventListener('DOMContentLoaded', function () {
186
189
  initializeMermaid();
187
190
  setupThemeObserver();
188
191
  });
@@ -192,12 +195,11 @@
192
195
  }
193
196
 
194
197
  // Handle tab switches - render mermaid in newly visible tabs
195
- document.addEventListener('click', function(e) {
198
+ document.addEventListener('click', function (e) {
196
199
  if (e.target.classList.contains('docmd-tabs-nav-item')) {
197
200
  // Wait a bit for tab content to be visible
198
201
  setTimeout(renderMermaidDiagrams, 100);
199
202
  }
200
203
  });
201
204
 
202
- })();
203
-
205
+ })();
@@ -0,0 +1,212 @@
1
+ // Source file from the docmd project — https://github.com/mgks/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
+ if (!searchModal) return;
17
+
18
+ const emptyStateHtml = '<div class="search-initial">Type to start searching...</div>';
19
+
20
+ // 1. Open/Close Logic
21
+ function openSearch() {
22
+ searchModal.style.display = 'flex';
23
+ searchInput.focus();
24
+
25
+ if (!searchInput.value.trim()) {
26
+ searchResults.innerHTML = emptyStateHtml;
27
+ selectedIndex = -1;
28
+ }
29
+
30
+ if (!isIndexLoaded) loadIndex();
31
+ }
32
+
33
+ function closeSearch() {
34
+ searchModal.style.display = 'none';
35
+ selectedIndex = -1; // Reset selection
36
+ }
37
+
38
+ // 2. Keyboard Navigation & Shortcuts
39
+ document.addEventListener('keydown', (e) => {
40
+ // Open: Cmd+K / Ctrl+K
41
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
42
+ e.preventDefault();
43
+ if (searchModal.style.display === 'flex') {
44
+ closeSearch();
45
+ } else {
46
+ openSearch();
47
+ }
48
+ }
49
+
50
+ // Context: Only handle these if search is open
51
+ if (searchModal.style.display === 'flex') {
52
+ const items = searchResults.querySelectorAll('.search-result-item');
53
+
54
+ if (e.key === 'Escape') {
55
+ e.preventDefault();
56
+ closeSearch();
57
+ }
58
+ else if (e.key === 'ArrowDown') {
59
+ e.preventDefault();
60
+ if (items.length === 0) return;
61
+ selectedIndex = (selectedIndex + 1) % items.length;
62
+ updateSelection(items);
63
+ }
64
+ else if (e.key === 'ArrowUp') {
65
+ e.preventDefault();
66
+ if (items.length === 0) return;
67
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
68
+ updateSelection(items);
69
+ }
70
+ else if (e.key === 'Enter') {
71
+ e.preventDefault();
72
+ if (selectedIndex >= 0 && items[selectedIndex]) {
73
+ items[selectedIndex].click();
74
+ } else if (items.length > 0) {
75
+ // If nothing selected but results exist, click the first one on Enter
76
+ items[0].click();
77
+ }
78
+ }
79
+ }
80
+ });
81
+
82
+ function updateSelection(items) {
83
+ items.forEach((item, index) => {
84
+ if (index === selectedIndex) {
85
+ item.classList.add('selected');
86
+ item.scrollIntoView({ block: 'nearest' });
87
+ } else {
88
+ item.classList.remove('selected');
89
+ }
90
+ });
91
+ }
92
+
93
+ // Click handlers
94
+ document.querySelectorAll('.docmd-search-trigger').forEach(btn => {
95
+ btn.addEventListener('click', openSearch);
96
+ });
97
+
98
+ searchModal.addEventListener('click', (e) => {
99
+ if (e.target === searchModal) closeSearch();
100
+ });
101
+
102
+ // 3. Index Loading Logic
103
+ async function loadIndex() {
104
+ try {
105
+ const basePath = document.documentElement.getAttribute('data-base-url') || '/';
106
+ const response = await fetch(`${basePath}search-index.json`);
107
+ if (!response.ok) throw new Error(response.status);
108
+
109
+ const jsonString = await response.text();
110
+
111
+ miniSearch = MiniSearch.loadJSON(jsonString, {
112
+ fields: ['title', 'headings', 'text'],
113
+ storeFields: ['title', 'id', 'text'],
114
+ searchOptions: {
115
+ fuzzy: 0.2,
116
+ prefix: true,
117
+ boost: { title: 2, headings: 1.5 }
118
+ }
119
+ });
120
+
121
+ isIndexLoaded = true;
122
+ // console.log('Search index loaded');
123
+
124
+ if (searchInput.value.trim()) {
125
+ searchInput.dispatchEvent(new Event('input'));
126
+ }
127
+
128
+ } catch (e) {
129
+ console.error('Failed to load search index', e);
130
+ searchResults.innerHTML = '<div class="search-error">Failed to load search index.</div>';
131
+ }
132
+ }
133
+
134
+ // Helper: Snippets (Same as before)
135
+ function getSnippet(text, query) {
136
+ if (!text) return '';
137
+ const terms = query.split(/\s+/).filter(t => t.length > 2);
138
+ const lowerText = text.toLowerCase();
139
+ let bestIndex = -1;
140
+
141
+ for (const term of terms) {
142
+ const idx = lowerText.indexOf(term.toLowerCase());
143
+ if (idx >= 0) { bestIndex = idx; break; }
144
+ }
145
+
146
+ const contextSize = 60;
147
+ let start = 0;
148
+ let end = 120;
149
+
150
+ if (bestIndex >= 0) {
151
+ start = Math.max(0, bestIndex - contextSize);
152
+ end = Math.min(text.length, bestIndex + contextSize);
153
+ }
154
+
155
+ let snippet = text.substring(start, end);
156
+ if (start > 0) snippet = '...' + snippet;
157
+ if (end < text.length) snippet = snippet + '...';
158
+
159
+ const safeTerms = terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
160
+ if (safeTerms) {
161
+ const highlightRegex = new RegExp(`(${safeTerms})`, 'gi');
162
+ snippet = snippet.replace(highlightRegex, '<mark>$1</mark>');
163
+ }
164
+ return snippet;
165
+ }
166
+
167
+ // 4. Search Execution
168
+ searchInput.addEventListener('input', (e) => {
169
+ const query = e.target.value.trim();
170
+ selectedIndex = -1; // Reset selection on new input
171
+
172
+ if (!query) {
173
+ searchResults.innerHTML = emptyStateHtml;
174
+ return;
175
+ }
176
+
177
+ if (!isIndexLoaded) return;
178
+
179
+ const results = miniSearch.search(query);
180
+ renderResults(results, query);
181
+ });
182
+
183
+ function renderResults(results, query) {
184
+ if (results.length === 0) {
185
+ searchResults.innerHTML = '<div class="search-no-results">No results found.</div>';
186
+ return;
187
+ }
188
+
189
+ const html = results.slice(0, 10).map((result, index) => {
190
+ const snippet = getSnippet(result.text, query);
191
+ // Add data-index for mouse interaction tracking if needed
192
+ return `
193
+ <a href="/${result.id}" class="search-result-item" data-index="${index}" onclick="window.closeDocmdSearch()">
194
+ <div class="search-result-title">${result.title}</div>
195
+ <div class="search-result-preview">${snippet}</div>
196
+ </a>
197
+ `;
198
+ }).join('');
199
+
200
+ searchResults.innerHTML = html;
201
+
202
+ // Optional: Allow mouse hover to update selectedIndex
203
+ searchResults.querySelectorAll('.search-result-item').forEach((item, idx) => {
204
+ item.addEventListener('mouseenter', () => {
205
+ selectedIndex = idx;
206
+ updateSelection(searchResults.querySelectorAll('.search-result-item'));
207
+ });
208
+ });
209
+ }
210
+
211
+ window.closeDocmdSearch = closeSearch;
212
+ })();