@mgks/docmd 0.3.3 → 0.3.4

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.
@@ -0,0 +1,285 @@
1
+ // Source file from the docmd project — https://github.com/mgks/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
+ let navStates = {};
13
+ try {
14
+ // Use sessionStorage to remember state only for the current session
15
+ navStates = JSON.parse(sessionStorage.getItem('docmd-nav-states')) || {};
16
+ } catch (e) { /* silent fail */ }
17
+
18
+ nav.querySelectorAll('li.collapsible').forEach(item => {
19
+ const navId = item.dataset.navId;
20
+ const anchor = item.querySelector('a');
21
+ const submenu = item.querySelector('.submenu');
22
+
23
+ if (!navId || !anchor || !submenu) return;
24
+
25
+ const isParentActive = item.classList.contains('active-parent');
26
+ // Default to expanded if it's a parent of the active page, otherwise check stored state.
27
+ let isExpanded = isParentActive || (navStates[navId] === true);
28
+
29
+ const toggleSubmenu = (expand) => {
30
+ item.setAttribute('aria-expanded', expand);
31
+ submenu.style.display = expand ? 'block' : 'none';
32
+ navStates[navId] = expand;
33
+ sessionStorage.setItem('docmd-nav-states', JSON.stringify(navStates));
34
+ };
35
+
36
+ // Set initial state on page load
37
+ toggleSubmenu(isExpanded);
38
+
39
+ anchor.addEventListener('click', (e) => {
40
+ const currentExpanded = item.getAttribute('aria-expanded') === 'true';
41
+ const href = anchor.getAttribute('href');
42
+ const isPlaceholder = !href || href === '#' || href === '';
43
+
44
+ if (!currentExpanded) {
45
+ toggleSubmenu(true);
46
+ } else if (isPlaceholder || e.target.closest('.collapse-icon')) {
47
+ toggleSubmenu(false);
48
+ }
49
+
50
+ if (isPlaceholder || e.target.closest('.collapse-icon')) {
51
+ e.preventDefault();
52
+ }
53
+ });
54
+
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
+ });*/
68
+ });
69
+ }
70
+
71
+ // --- Mobile Menu Logic ---
72
+ function initializeMobileMenus() {
73
+ // 1. Sidebar Toggle
74
+ const sidebarBtn = document.querySelector('.sidebar-menu-button');
75
+ const sidebar = document.querySelector('.sidebar');
76
+
77
+ if (sidebarBtn && sidebar) {
78
+ sidebarBtn.addEventListener('click', (e) => {
79
+ e.stopPropagation(); // Prevent bubbling
80
+ sidebar.classList.toggle('mobile-expanded');
81
+ });
82
+ }
83
+
84
+ // 2. TOC Toggle
85
+ const tocBtn = document.querySelector('.toc-menu-button');
86
+ const tocContainer = document.querySelector('.toc-container');
87
+ // Also allow clicking the title text to toggle
88
+ const tocTitle = document.querySelector('.toc-title');
89
+
90
+ const toggleToc = (e) => {
91
+ // Only engage on mobile view (check if button is visible)
92
+ if (window.getComputedStyle(tocBtn).display === 'none') return;
93
+
94
+ e.stopPropagation();
95
+ tocContainer.classList.toggle('mobile-expanded');
96
+ };
97
+
98
+ if (tocBtn && tocContainer) {
99
+ tocBtn.addEventListener('click', toggleToc);
100
+ if (tocTitle) {
101
+ tocTitle.addEventListener('click', toggleToc);
102
+ }
103
+ }
104
+ }
105
+
106
+ // --- Sidebar Scroll Preservation ---
107
+ function initializeSidebarScroll() {
108
+ const sidebar = document.querySelector('.sidebar');
109
+ if (!sidebar) return;
110
+
111
+ setTimeout(() => {
112
+ const activeElement = sidebar.querySelector('a.active') || sidebar.querySelector('.active-parent > a');
113
+
114
+ if (activeElement) {
115
+ const sidebarRect = sidebar.getBoundingClientRect();
116
+ const elementRect = activeElement.getBoundingClientRect();
117
+
118
+ // Check if the element's top or bottom is outside the sidebar's visible area
119
+ const isNotInView = elementRect.top < sidebarRect.top || elementRect.bottom > sidebarRect.bottom;
120
+
121
+ if (isNotInView) {
122
+ activeElement.scrollIntoView({
123
+ behavior: 'auto',
124
+ block: 'center',
125
+ inline: 'nearest'
126
+ });
127
+ }
128
+ }
129
+ }, 10);
130
+ }
131
+
132
+ // --- Theme Toggle Logic ---
133
+ function setupThemeToggleListener() {
134
+ const themeToggleButton = document.getElementById('theme-toggle-button');
135
+
136
+ function applyTheme(theme) {
137
+ document.documentElement.setAttribute('data-theme', theme);
138
+ document.body.setAttribute('data-theme', theme);
139
+ localStorage.setItem('docmd-theme', theme);
140
+
141
+ // Switch highlight.js theme
142
+ const highlightThemeLink = document.getElementById('highlight-theme');
143
+ if (highlightThemeLink) {
144
+ const newHref = highlightThemeLink.getAttribute('data-base-href') + `docmd-highlight-${theme}.css`;
145
+ highlightThemeLink.setAttribute('href', newHref);
146
+ }
147
+ }
148
+
149
+ // Add click listener to the toggle button
150
+ if (themeToggleButton) {
151
+ themeToggleButton.addEventListener('click', () => {
152
+ const currentTheme = document.documentElement.getAttribute('data-theme');
153
+ const newTheme = currentTheme === 'light' ? 'dark' : 'light';
154
+ applyTheme(newTheme);
155
+ });
156
+ }
157
+ }
158
+
159
+ // --- Sidebar Collapse Logic ---
160
+ function initializeSidebarToggle() {
161
+ const toggleButton = document.getElementById('sidebar-toggle-button');
162
+ const body = document.body;
163
+
164
+ if (!body.classList.contains('sidebar-collapsible') || !toggleButton) {
165
+ return;
166
+ }
167
+
168
+ const defaultConfigCollapsed = body.dataset.defaultCollapsed === 'true';
169
+ let isCollapsed = localStorage.getItem('docmd-sidebar-collapsed');
170
+
171
+ if (isCollapsed === null) {
172
+ isCollapsed = defaultConfigCollapsed;
173
+ } else {
174
+ isCollapsed = isCollapsed === 'true';
175
+ }
176
+
177
+ if (isCollapsed) {
178
+ body.classList.add('sidebar-collapsed');
179
+ }
180
+
181
+ toggleButton.addEventListener('click', () => {
182
+ body.classList.toggle('sidebar-collapsed');
183
+ const currentlyCollapsed = body.classList.contains('sidebar-collapsed');
184
+ localStorage.setItem('docmd-sidebar-collapsed', currentlyCollapsed);
185
+ });
186
+ }
187
+
188
+ // --- Tabs Container Logic ---
189
+ function initializeTabs() {
190
+ document.querySelectorAll('.docmd-tabs').forEach(tabsContainer => {
191
+ const navItems = tabsContainer.querySelectorAll('.docmd-tabs-nav-item');
192
+ const tabPanes = tabsContainer.querySelectorAll('.docmd-tab-pane');
193
+
194
+ navItems.forEach((navItem, index) => {
195
+ navItem.addEventListener('click', () => {
196
+ navItems.forEach(item => item.classList.remove('active'));
197
+ tabPanes.forEach(pane => pane.classList.remove('active'));
198
+
199
+ navItem.classList.add('active');
200
+ if (tabPanes[index]) {
201
+ tabPanes[index].classList.add('active');
202
+ }
203
+ });
204
+ });
205
+ });
206
+ }
207
+
208
+ // --- Copy Code Button Logic ---
209
+ function initializeCopyCodeButtons() {
210
+ if (document.body.dataset.copyCodeEnabled !== 'true') {
211
+ return;
212
+ }
213
+
214
+ 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>`;
215
+ 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>`;
216
+
217
+ document.querySelectorAll('pre').forEach(preElement => {
218
+ const codeElement = preElement.querySelector('code');
219
+ if (!codeElement) return;
220
+
221
+ // Create a wrapper div around the pre element
222
+ const wrapper = document.createElement('div');
223
+ wrapper.style.position = 'relative';
224
+ wrapper.style.display = 'block';
225
+
226
+ // Insert the wrapper before the pre element
227
+ preElement.parentNode.insertBefore(wrapper, preElement);
228
+
229
+ // Move the pre element into the wrapper
230
+ wrapper.appendChild(preElement);
231
+
232
+ // Remove the relative positioning from pre since wrapper handles it
233
+ preElement.style.position = 'static';
234
+
235
+ const copyButton = document.createElement('button');
236
+ copyButton.className = 'copy-code-button';
237
+ copyButton.innerHTML = copyIconSvg;
238
+ copyButton.setAttribute('aria-label', 'Copy code to clipboard');
239
+ wrapper.appendChild(copyButton);
240
+
241
+ copyButton.addEventListener('click', () => {
242
+ navigator.clipboard.writeText(codeElement.innerText).then(() => {
243
+ copyButton.innerHTML = checkIconSvg;
244
+ copyButton.classList.add('copied');
245
+ setTimeout(() => {
246
+ copyButton.innerHTML = copyIconSvg;
247
+ copyButton.classList.remove('copied');
248
+ }, 2000);
249
+ }).catch(err => {
250
+ console.error('Failed to copy text: ', err);
251
+ copyButton.innerText = 'Error';
252
+ });
253
+ });
254
+ });
255
+ }
256
+
257
+ // --- Theme Sync Function ---
258
+ function syncBodyTheme() {
259
+ const currentTheme = document.documentElement.getAttribute('data-theme');
260
+ if (currentTheme && document.body) {
261
+ document.body.setAttribute('data-theme', currentTheme);
262
+ }
263
+
264
+ // Also ensure highlight CSS matches the current theme
265
+ const highlightThemeLink = document.getElementById('highlight-theme');
266
+ if (highlightThemeLink && currentTheme) {
267
+ const baseHref = highlightThemeLink.getAttribute('data-base-href');
268
+ if (baseHref) {
269
+ const newHref = baseHref + `docmd-highlight-${currentTheme}.css`;
270
+ highlightThemeLink.setAttribute('href', newHref);
271
+ }
272
+ }
273
+ }
274
+
275
+ // --- Main Execution ---
276
+ document.addEventListener('DOMContentLoaded', () => {
277
+ syncBodyTheme(); // Sync body theme with html theme
278
+ setupThemeToggleListener();
279
+ initializeSidebarToggle();
280
+ initializeTabs();
281
+ initializeCopyCodeButtons();
282
+ initializeCollapsibleNav();
283
+ initializeMobileMenus();
284
+ initializeSidebarScroll();
285
+ });
@@ -0,0 +1,205 @@
1
+ // Source file from the docmd project — https://github.com/mgks/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
+ })();