@mgks/docmd 0.2.0 → 0.2.2
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/assets/css/welcome.css +6 -66
- package/config.js +14 -5
- package/docs/configuration.md +15 -6
- package/docs/content/containers/steps.md +2 -2
- package/docs/content/custom-containers.md +24 -0
- package/docs/content/no-style-example.md +2 -0
- package/docs/content/no-style-pages.md +52 -28
- package/docs/index.md +49 -18
- package/docs/plugins/seo.md +80 -33
- package/package.json +13 -7
- package/src/assets/css/docmd-main.css +6 -1168
- package/src/assets/css/docmd-theme-retro.css +3 -806
- package/src/assets/css/docmd-theme-ruby.css +7 -617
- package/src/assets/css/docmd-theme-sky.css +7 -650
- package/src/assets/js/docmd-image-lightbox.js +5 -1
- package/src/assets/js/docmd-main.js +164 -29
- package/src/commands/build.js +75 -134
- package/src/commands/dev.js +2 -1
- package/src/commands/init.js +6 -1
- package/src/core/config-loader.js +2 -0
- package/src/core/file-processor.js +147 -104
- package/src/core/html-generator.js +31 -12
- package/src/core/icon-renderer.js +3 -2
- package/src/plugins/analytics.js +5 -1
- package/src/plugins/seo.js +114 -66
- package/src/plugins/sitemap.js +6 -0
- package/src/templates/layout.ejs +8 -2
- package/src/templates/navigation.ejs +69 -98
- package/src/templates/no-style.ejs +23 -6
- package/src/templates/partials/theme-init.js +26 -0
|
@@ -1,52 +1,118 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* 1. Light/Dark theme toggling and persistence.
|
|
6
|
-
* 2. Sidebar expand/collapse functionality and persistence.
|
|
7
|
-
* 3. Tabs container interaction.
|
|
1
|
+
// Source file from the docmd project — https://github.com/mgks/docmd
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Main client-side script for docmd UI interactions
|
|
8
5
|
*/
|
|
6
|
+
// --- Collapsible Navigation Logic ---
|
|
7
|
+
function initializeCollapsibleNav() {
|
|
8
|
+
const nav = document.querySelector('.sidebar-nav');
|
|
9
|
+
if (!nav) return;
|
|
10
|
+
|
|
11
|
+
let navStates = {};
|
|
12
|
+
try {
|
|
13
|
+
// Use sessionStorage to remember state only for the current session
|
|
14
|
+
navStates = JSON.parse(sessionStorage.getItem('docmd-nav-states')) || {};
|
|
15
|
+
} catch (e) { /* silent fail */ }
|
|
16
|
+
|
|
17
|
+
nav.querySelectorAll('li.collapsible').forEach(item => {
|
|
18
|
+
const navId = item.dataset.navId;
|
|
19
|
+
const anchor = item.querySelector('a');
|
|
20
|
+
const submenu = item.querySelector('.submenu');
|
|
21
|
+
|
|
22
|
+
if (!navId || !anchor || !submenu) return;
|
|
23
|
+
|
|
24
|
+
const isParentActive = item.classList.contains('active-parent');
|
|
25
|
+
// Default to expanded if it's a parent of the active page, otherwise check stored state.
|
|
26
|
+
let isExpanded = isParentActive || (navStates[navId] === true);
|
|
27
|
+
|
|
28
|
+
const toggleSubmenu = (expand) => {
|
|
29
|
+
item.setAttribute('aria-expanded', expand);
|
|
30
|
+
submenu.style.display = expand ? 'block' : 'none';
|
|
31
|
+
navStates[navId] = expand;
|
|
32
|
+
sessionStorage.setItem('docmd-nav-states', JSON.stringify(navStates));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Set initial state on page load
|
|
36
|
+
toggleSubmenu(isExpanded);
|
|
37
|
+
|
|
38
|
+
anchor.addEventListener('click', (e) => {
|
|
39
|
+
// If the click target is the icon, ALWAYS prevent navigation and toggle.
|
|
40
|
+
if (e.target.closest('.collapse-icon')) {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
|
|
43
|
+
}
|
|
44
|
+
// If the link is just a placeholder, also prevent navigation and toggle.
|
|
45
|
+
else if (anchor.getAttribute('href') === '#') {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
toggleSubmenu(item.getAttribute('aria-expanded') !== 'true');
|
|
48
|
+
}
|
|
49
|
+
// Otherwise, let the click proceed to navigate to the link.
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- Sidebar Scroll Preservation ---
|
|
55
|
+
function initializeSidebarScroll() {
|
|
56
|
+
const sidebar = document.querySelector('.sidebar');
|
|
57
|
+
if (!sidebar) return;
|
|
58
|
+
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
const activeElement = sidebar.querySelector('a.active') || sidebar.querySelector('.active-parent > a');
|
|
61
|
+
|
|
62
|
+
if (activeElement) {
|
|
63
|
+
const sidebarRect = sidebar.getBoundingClientRect();
|
|
64
|
+
const elementRect = activeElement.getBoundingClientRect();
|
|
65
|
+
|
|
66
|
+
// Check if the element's top or bottom is outside the sidebar's visible area
|
|
67
|
+
const isNotInView = elementRect.top < sidebarRect.top || elementRect.bottom > sidebarRect.bottom;
|
|
9
68
|
|
|
10
|
-
|
|
11
|
-
|
|
69
|
+
if (isNotInView) {
|
|
70
|
+
activeElement.scrollIntoView({
|
|
71
|
+
behavior: 'auto',
|
|
72
|
+
block: 'center',
|
|
73
|
+
inline: 'nearest'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}, 10);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Theme Toggle Logic ---
|
|
81
|
+
function setupThemeToggleListener() {
|
|
12
82
|
const themeToggleButton = document.getElementById('theme-toggle-button');
|
|
13
83
|
|
|
14
|
-
|
|
15
|
-
|
|
84
|
+
function applyTheme(theme) {
|
|
85
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
16
86
|
document.body.setAttribute('data-theme', theme);
|
|
17
|
-
|
|
18
|
-
|
|
87
|
+
localStorage.setItem('docmd-theme', theme);
|
|
88
|
+
|
|
89
|
+
// Switch highlight.js theme
|
|
90
|
+
const highlightThemeLink = document.getElementById('highlight-theme');
|
|
91
|
+
if (highlightThemeLink) {
|
|
92
|
+
const newHref = highlightThemeLink.getAttribute('data-base-href') + `docmd-highlight-${theme}.css`;
|
|
93
|
+
highlightThemeLink.setAttribute('href', newHref);
|
|
19
94
|
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Set the initial theme on page load
|
|
23
|
-
const storedTheme = localStorage.getItem('docmd-theme');
|
|
24
|
-
const initialTheme = storedTheme || document.body.getAttribute('data-theme') || 'light';
|
|
25
|
-
applyTheme(initialTheme, true);
|
|
95
|
+
}
|
|
26
96
|
|
|
27
97
|
// Add click listener to the toggle button
|
|
28
98
|
if (themeToggleButton) {
|
|
29
99
|
themeToggleButton.addEventListener('click', () => {
|
|
30
|
-
const currentTheme = document.
|
|
31
|
-
const newTheme = currentTheme
|
|
32
|
-
? currentTheme.replace('dark', 'light')
|
|
33
|
-
: currentTheme.replace('light', 'dark');
|
|
100
|
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
101
|
+
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
|
34
102
|
applyTheme(newTheme);
|
|
35
103
|
});
|
|
36
104
|
}
|
|
37
105
|
}
|
|
38
106
|
|
|
39
|
-
// ---
|
|
107
|
+
// --- Sidebar Collapse Logic ---
|
|
40
108
|
function initializeSidebarToggle() {
|
|
41
109
|
const toggleButton = document.getElementById('sidebar-toggle-button');
|
|
42
110
|
const body = document.body;
|
|
43
111
|
|
|
44
|
-
// Only run if the sidebar is configured to be collapsible
|
|
45
112
|
if (!body.classList.contains('sidebar-collapsible') || !toggleButton) {
|
|
46
113
|
return;
|
|
47
114
|
}
|
|
48
115
|
|
|
49
|
-
// Set initial state from localStorage or config default
|
|
50
116
|
const defaultConfigCollapsed = body.dataset.defaultCollapsed === 'true';
|
|
51
117
|
let isCollapsed = localStorage.getItem('docmd-sidebar-collapsed');
|
|
52
118
|
|
|
@@ -60,7 +126,6 @@ function initializeSidebarToggle() {
|
|
|
60
126
|
body.classList.add('sidebar-collapsed');
|
|
61
127
|
}
|
|
62
128
|
|
|
63
|
-
// Add click listener to the toggle button
|
|
64
129
|
toggleButton.addEventListener('click', () => {
|
|
65
130
|
body.classList.toggle('sidebar-collapsed');
|
|
66
131
|
const currentlyCollapsed = body.classList.contains('sidebar-collapsed');
|
|
@@ -68,7 +133,7 @@ function initializeSidebarToggle() {
|
|
|
68
133
|
});
|
|
69
134
|
}
|
|
70
135
|
|
|
71
|
-
// ---
|
|
136
|
+
// --- Tabs Container Logic ---
|
|
72
137
|
function initializeTabs() {
|
|
73
138
|
document.querySelectorAll('.docmd-tabs').forEach(tabsContainer => {
|
|
74
139
|
const navItems = tabsContainer.querySelectorAll('.docmd-tabs-nav-item');
|
|
@@ -88,10 +153,80 @@ function initializeTabs() {
|
|
|
88
153
|
});
|
|
89
154
|
}
|
|
90
155
|
|
|
156
|
+
// --- Copy Code Button Logic ---
|
|
157
|
+
function initializeCopyCodeButtons() {
|
|
158
|
+
if (document.body.dataset.copyCodeEnabled !== 'true') {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
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>`;
|
|
163
|
+
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>`;
|
|
164
|
+
|
|
165
|
+
document.querySelectorAll('pre').forEach(preElement => {
|
|
166
|
+
const codeElement = preElement.querySelector('code');
|
|
167
|
+
if (!codeElement) return;
|
|
168
|
+
|
|
169
|
+
// Create a wrapper div around the pre element
|
|
170
|
+
const wrapper = document.createElement('div');
|
|
171
|
+
wrapper.style.position = 'relative';
|
|
172
|
+
wrapper.style.display = 'block';
|
|
173
|
+
|
|
174
|
+
// Insert the wrapper before the pre element
|
|
175
|
+
preElement.parentNode.insertBefore(wrapper, preElement);
|
|
176
|
+
|
|
177
|
+
// Move the pre element into the wrapper
|
|
178
|
+
wrapper.appendChild(preElement);
|
|
179
|
+
|
|
180
|
+
// Remove the relative positioning from pre since wrapper handles it
|
|
181
|
+
preElement.style.position = 'static';
|
|
182
|
+
|
|
183
|
+
const copyButton = document.createElement('button');
|
|
184
|
+
copyButton.className = 'copy-code-button';
|
|
185
|
+
copyButton.innerHTML = copyIconSvg;
|
|
186
|
+
copyButton.setAttribute('aria-label', 'Copy code to clipboard');
|
|
187
|
+
wrapper.appendChild(copyButton);
|
|
188
|
+
|
|
189
|
+
copyButton.addEventListener('click', () => {
|
|
190
|
+
navigator.clipboard.writeText(codeElement.innerText).then(() => {
|
|
191
|
+
copyButton.innerHTML = checkIconSvg;
|
|
192
|
+
copyButton.classList.add('copied');
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
copyButton.innerHTML = copyIconSvg;
|
|
195
|
+
copyButton.classList.remove('copied');
|
|
196
|
+
}, 2000);
|
|
197
|
+
}).catch(err => {
|
|
198
|
+
console.error('Failed to copy text: ', err);
|
|
199
|
+
copyButton.innerText = 'Error';
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --- Theme Sync Function ---
|
|
206
|
+
function syncBodyTheme() {
|
|
207
|
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
208
|
+
if (currentTheme && document.body) {
|
|
209
|
+
document.body.setAttribute('data-theme', currentTheme);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Also ensure highlight CSS matches the current theme
|
|
213
|
+
const highlightThemeLink = document.getElementById('highlight-theme');
|
|
214
|
+
if (highlightThemeLink && currentTheme) {
|
|
215
|
+
const baseHref = highlightThemeLink.getAttribute('data-base-href');
|
|
216
|
+
if (baseHref) {
|
|
217
|
+
const newHref = baseHref + `docmd-highlight-${currentTheme}.css`;
|
|
218
|
+
highlightThemeLink.setAttribute('href', newHref);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
91
222
|
|
|
92
223
|
// --- Main Execution ---
|
|
93
224
|
document.addEventListener('DOMContentLoaded', () => {
|
|
94
|
-
|
|
225
|
+
syncBodyTheme(); // Sync body theme with html theme
|
|
226
|
+
setupThemeToggleListener();
|
|
95
227
|
initializeSidebarToggle();
|
|
96
228
|
initializeTabs();
|
|
229
|
+
initializeCopyCodeButtons();
|
|
230
|
+
initializeCollapsibleNav();
|
|
231
|
+
initializeSidebarScroll();
|
|
97
232
|
});
|
package/src/commands/build.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Source file from the docmd project — https://github.com/mgks/docmd
|
|
2
|
+
|
|
2
3
|
const fs = require('fs-extra');
|
|
3
4
|
const path = require('path');
|
|
4
5
|
const { loadConfig } = require('../core/config-loader');
|
|
5
|
-
const { processMarkdownFile, findMarkdownFiles } = require('../core/file-processor');
|
|
6
|
+
const { createMarkdownItInstance, processMarkdownFile, findMarkdownFiles } = require('../core/file-processor');
|
|
6
7
|
const { generateHtmlPage, generateNavigationHtml } = require('../core/html-generator');
|
|
7
|
-
const { renderIcon, clearWarnedIcons } = require('../core/icon-renderer');
|
|
8
|
-
const { generateSitemap } = require('../plugins/sitemap');
|
|
9
|
-
const { version } = require('../../package.json');
|
|
10
|
-
const matter = require('gray-matter');
|
|
8
|
+
const { renderIcon, clearWarnedIcons } = require('../core/icon-renderer');
|
|
9
|
+
const { generateSitemap } = require('../plugins/sitemap');
|
|
10
|
+
const { version } = require('../../package.json');
|
|
11
|
+
const matter = require('gray-matter');
|
|
12
|
+
const MarkdownIt = require('markdown-it');
|
|
13
|
+
const hljs = require('highlight.js');
|
|
11
14
|
|
|
12
15
|
// Debug function to log navigation information
|
|
13
16
|
function logNavigationPaths(pagePath, navPath, normalizedPath) {
|
|
@@ -55,7 +58,8 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
55
58
|
const CWD = process.cwd();
|
|
56
59
|
const SRC_DIR = path.resolve(CWD, config.srcDir);
|
|
57
60
|
const OUTPUT_DIR = path.resolve(CWD, config.outputDir);
|
|
58
|
-
const USER_ASSETS_DIR = path.resolve(CWD, 'assets');
|
|
61
|
+
const USER_ASSETS_DIR = path.resolve(CWD, 'assets');
|
|
62
|
+
const md = createMarkdownItInstance(config);
|
|
59
63
|
|
|
60
64
|
if (!await fs.pathExists(SRC_DIR)) {
|
|
61
65
|
throw new Error(`Source directory not found: ${formatPathForDisplay(SRC_DIR, CWD)}`);
|
|
@@ -171,38 +175,44 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
171
175
|
// Array to collect information about all processed pages for sitemap
|
|
172
176
|
const processedPages = [];
|
|
173
177
|
|
|
178
|
+
// Set to track processed files for dev mode
|
|
179
|
+
const processedFiles = new Set();
|
|
180
|
+
|
|
174
181
|
// Find all Markdown files in the source directory
|
|
175
182
|
const markdownFiles = await findMarkdownFiles(SRC_DIR);
|
|
176
183
|
if (!options.isDev) {
|
|
177
184
|
console.log(`📄 Found ${markdownFiles.length} markdown files.`);
|
|
178
185
|
}
|
|
179
|
-
|
|
186
|
+
|
|
180
187
|
// Process each Markdown file
|
|
181
|
-
const processedFiles = new Set(); // Track processed files to avoid double processing
|
|
182
|
-
|
|
183
188
|
for (const filePath of markdownFiles) {
|
|
184
189
|
try {
|
|
185
|
-
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
186
|
-
const { data: frontmatter, content } = matter(fileContent);
|
|
187
|
-
|
|
188
|
-
// Skip this file if it's already been processed and noDoubleProcessing is true
|
|
189
190
|
const relativePath = path.relative(SRC_DIR, filePath);
|
|
191
|
+
|
|
192
|
+
// Skip file if already processed in this dev build cycle
|
|
190
193
|
if (options.noDoubleProcessing && processedFiles.has(relativePath)) {
|
|
191
194
|
continue;
|
|
192
195
|
}
|
|
193
196
|
processedFiles.add(relativePath);
|
|
197
|
+
|
|
198
|
+
// Pass the md instance to the processor
|
|
199
|
+
const processedData = await processMarkdownFile(filePath, md, config);
|
|
200
|
+
|
|
201
|
+
// If processing failed (e.g., bad frontmatter), skip this file.
|
|
202
|
+
if (!processedData) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
194
205
|
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
const fileName = path.basename(relativePath);
|
|
198
|
-
const isIndexFile = fileName === 'index.md';
|
|
206
|
+
// Destructure the valid data
|
|
207
|
+
const { frontmatter: pageFrontmatter, htmlContent, headings } = processedData;
|
|
199
208
|
|
|
209
|
+
const isIndexFile = path.basename(relativePath) === 'index.md';
|
|
210
|
+
|
|
211
|
+
let outputHtmlPath;
|
|
200
212
|
if (isIndexFile) {
|
|
201
|
-
// For any index.md file (in root or subfolder), convert to index.html in the same folder
|
|
202
213
|
const dirPath = path.dirname(relativePath);
|
|
203
214
|
outputHtmlPath = path.join(dirPath, 'index.html');
|
|
204
215
|
} else {
|
|
205
|
-
// For non-index files, create a folder with index.html
|
|
206
216
|
outputHtmlPath = relativePath.replace(/\.md$/, '/index.html');
|
|
207
217
|
}
|
|
208
218
|
|
|
@@ -211,50 +221,24 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
211
221
|
const depth = outputHtmlPath.split(path.sep).length - 1;
|
|
212
222
|
const relativePathToRoot = depth > 0 ? '../'.repeat(depth) : './';
|
|
213
223
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// For no-style pages, ensure the HTML content is not escaped
|
|
220
|
-
// This is critical for the landing page and custom pages
|
|
221
|
-
finalHtmlContent = htmlContent;
|
|
222
|
-
|
|
223
|
-
// Log a message for debugging - but only for non-dev mode or verbose logging
|
|
224
|
-
if (!options.isDev) {
|
|
225
|
-
console.log(`📄 Processing no-style page: ${path.relative(CWD, filePath)}`);
|
|
224
|
+
let normalizedPath = path.relative(SRC_DIR, filePath).replace(/\\/g, '/');
|
|
225
|
+
if (path.basename(normalizedPath) === 'index.md') {
|
|
226
|
+
normalizedPath = path.dirname(normalizedPath);
|
|
227
|
+
if (normalizedPath === '.') {
|
|
228
|
+
normalizedPath = '';
|
|
226
229
|
}
|
|
230
|
+
} else {
|
|
231
|
+
normalizedPath = normalizedPath.replace(/\.md$/, '');
|
|
227
232
|
}
|
|
228
233
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// For index.md files, the nav path should be the directory itself with trailing slash
|
|
235
|
-
const dirPath = path.dirname(relativePath);
|
|
236
|
-
if (dirPath === '.') {
|
|
237
|
-
// Root index.md
|
|
238
|
-
currentPagePathForNav = 'index.html';
|
|
239
|
-
normalizedPath = '/';
|
|
240
|
-
} else {
|
|
241
|
-
// Subfolder index.md - simple format: directory-name/
|
|
242
|
-
currentPagePathForNav = dirPath + '/';
|
|
243
|
-
normalizedPath = '/' + dirPath;
|
|
244
|
-
}
|
|
245
|
-
} else {
|
|
246
|
-
// For non-index files, the path should be the file name with trailing slash
|
|
247
|
-
const pathWithoutExt = relativePath.replace(/\.md$/, '');
|
|
248
|
-
currentPagePathForNav = pathWithoutExt + '/';
|
|
249
|
-
normalizedPath = '/' + pathWithoutExt;
|
|
234
|
+
if (!normalizedPath.startsWith('/')) {
|
|
235
|
+
normalizedPath = '/' + normalizedPath;
|
|
236
|
+
}
|
|
237
|
+
if (normalizedPath.length > 1 && !normalizedPath.endsWith('/')) {
|
|
238
|
+
normalizedPath += '/';
|
|
250
239
|
}
|
|
251
|
-
|
|
252
|
-
// Convert Windows backslashes to forward slashes for web paths
|
|
253
|
-
currentPagePathForNav = currentPagePathForNav.replace(/\\/g, '/');
|
|
254
240
|
|
|
255
|
-
|
|
256
|
-
// Uncomment this line when debugging:
|
|
257
|
-
// logNavigationPaths(filePath, currentPagePathForNav, normalizedPath);
|
|
241
|
+
const currentPagePathForNav = normalizedPath;
|
|
258
242
|
|
|
259
243
|
const navigationHtml = await generateNavigationHtml(
|
|
260
244
|
config.navigation,
|
|
@@ -263,123 +247,80 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
263
247
|
config
|
|
264
248
|
);
|
|
265
249
|
|
|
266
|
-
// Find current page in navigation for prev/next links
|
|
267
250
|
let prevPage = null;
|
|
268
251
|
let nextPage = null;
|
|
269
252
|
let currentPageIndex = -1;
|
|
270
253
|
|
|
271
|
-
// Extract a flattened navigation array for prev/next links
|
|
272
254
|
const flatNavigation = [];
|
|
273
255
|
|
|
274
|
-
// Helper function to create a normalized path for navigation matching
|
|
275
256
|
function createNormalizedPath(item) {
|
|
276
257
|
if (!item.path) return null;
|
|
277
258
|
return item.path.startsWith('/') ? item.path : '/' + item.path;
|
|
278
259
|
}
|
|
279
260
|
|
|
280
|
-
function extractNavigationItems(items
|
|
261
|
+
function extractNavigationItems(items) {
|
|
281
262
|
if (!items || !Array.isArray(items)) return;
|
|
282
263
|
|
|
283
264
|
for (const item of items) {
|
|
284
|
-
if (item.external) continue;
|
|
265
|
+
if (item.external) continue;
|
|
285
266
|
|
|
286
|
-
// Only include items with paths (not section headers without links)
|
|
287
267
|
if (item.path) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
// For parent items with children, ensure path ends with / (folders)
|
|
292
|
-
// This helps with matching in the navigation template
|
|
293
|
-
if (item.children && item.children.length > 0) {
|
|
294
|
-
// If path from config doesn't end with slash, add it
|
|
295
|
-
if (!item.path.endsWith('/') && !normalizedPath.endsWith('/')) {
|
|
296
|
-
normalizedPath += '/';
|
|
297
|
-
}
|
|
268
|
+
let normalizedItemPath = createNormalizedPath(item);
|
|
269
|
+
if (item.children && !normalizedItemPath.endsWith('/')) {
|
|
270
|
+
normalizedItemPath += '/';
|
|
298
271
|
}
|
|
299
|
-
|
|
300
272
|
flatNavigation.push({
|
|
301
273
|
title: item.title,
|
|
302
|
-
path:
|
|
303
|
-
fullPath: item.path, // Original path as defined in config
|
|
304
|
-
isParent: item.children && item.children.length > 0 // Mark if it's a parent with children
|
|
274
|
+
path: normalizedItemPath,
|
|
305
275
|
});
|
|
306
276
|
}
|
|
307
277
|
|
|
308
|
-
// Process children (depth first to maintain document outline order)
|
|
309
278
|
if (item.children && Array.isArray(item.children)) {
|
|
310
|
-
extractNavigationItems(item.children
|
|
279
|
+
extractNavigationItems(item.children);
|
|
311
280
|
}
|
|
312
281
|
}
|
|
313
282
|
}
|
|
314
283
|
|
|
315
|
-
// Extract navigation items into flat array
|
|
316
284
|
extractNavigationItems(config.navigation);
|
|
317
285
|
|
|
318
|
-
// Find the current page in flatNavigation
|
|
319
286
|
currentPageIndex = flatNavigation.findIndex(item => {
|
|
320
|
-
|
|
321
|
-
if (item.path === normalizedPath) {
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Special handling for parent folders
|
|
287
|
+
if (item.path === normalizedPath) return true;
|
|
326
288
|
if (isIndexFile && item.path.endsWith('/')) {
|
|
327
|
-
|
|
328
|
-
const itemPathWithoutSlash = item.path.slice(0, -1);
|
|
329
|
-
return itemPathWithoutSlash === normalizedPath;
|
|
289
|
+
return item.path.slice(0, -1) === normalizedPath;
|
|
330
290
|
}
|
|
331
|
-
|
|
332
291
|
return false;
|
|
333
292
|
});
|
|
334
293
|
|
|
335
294
|
if (currentPageIndex >= 0) {
|
|
336
|
-
|
|
337
|
-
if (currentPageIndex
|
|
338
|
-
prevPage = flatNavigation[currentPageIndex - 1];
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (currentPageIndex < flatNavigation.length - 1) {
|
|
342
|
-
nextPage = flatNavigation[currentPageIndex + 1];
|
|
343
|
-
}
|
|
295
|
+
if (currentPageIndex > 0) prevPage = flatNavigation[currentPageIndex - 1];
|
|
296
|
+
if (currentPageIndex < flatNavigation.length - 1) nextPage = flatNavigation[currentPageIndex + 1];
|
|
344
297
|
}
|
|
345
298
|
|
|
346
|
-
// Convert page paths to proper URLs for links
|
|
347
299
|
if (prevPage) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
} else {
|
|
352
|
-
// Remove leading slash and ensure clean path
|
|
353
|
-
const cleanPath = prevPage.path.substring(1).replace(/\/+$/, '');
|
|
354
|
-
prevPage.url = relativePathToRoot + cleanPath + '/';
|
|
355
|
-
}
|
|
300
|
+
const cleanPath = prevPage.path.startsWith('/') ? prevPage.path.substring(1) : prevPage.path;
|
|
301
|
+
prevPage.url = relativePathToRoot + (cleanPath.endsWith('/') ? cleanPath : cleanPath + '/');
|
|
302
|
+
if (prevPage.path === '/') prevPage.url = relativePathToRoot;
|
|
356
303
|
}
|
|
357
304
|
|
|
358
305
|
if (nextPage) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
} else {
|
|
363
|
-
// Remove leading slash and ensure clean path
|
|
364
|
-
const cleanPath = nextPage.path.substring(1).replace(/\/+$/, '');
|
|
365
|
-
nextPage.url = relativePathToRoot + cleanPath + '/';
|
|
366
|
-
}
|
|
306
|
+
const cleanPath = nextPage.path.startsWith('/') ? nextPage.path.substring(1) : nextPage.path;
|
|
307
|
+
nextPage.url = relativePathToRoot + (cleanPath.endsWith('/') ? cleanPath : cleanPath + '/');
|
|
308
|
+
if (nextPage.path === '/') nextPage.url = relativePathToRoot;
|
|
367
309
|
}
|
|
368
310
|
|
|
369
311
|
const pageDataForTemplate = {
|
|
370
|
-
content:
|
|
312
|
+
content: htmlContent,
|
|
371
313
|
pageTitle: pageFrontmatter.title || 'Untitled',
|
|
372
314
|
siteTitle: config.siteTitle,
|
|
373
315
|
navigationHtml,
|
|
374
316
|
relativePathToRoot: relativePathToRoot,
|
|
375
|
-
config: config,
|
|
317
|
+
config: config,
|
|
376
318
|
frontmatter: pageFrontmatter,
|
|
377
|
-
outputPath: outputHtmlPath,
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
headings: headings || [], // Pass headings for TOC
|
|
319
|
+
outputPath: outputHtmlPath.replace(/\\/g, '/'),
|
|
320
|
+
prevPage: prevPage,
|
|
321
|
+
nextPage: nextPage,
|
|
322
|
+
currentPagePath: normalizedPath,
|
|
323
|
+
headings: headings || [],
|
|
383
324
|
};
|
|
384
325
|
|
|
385
326
|
const pageHtml = await generateHtmlPage(pageDataForTemplate);
|
|
@@ -387,16 +328,16 @@ async function buildSite(configPath, options = { isDev: false, preserve: false,
|
|
|
387
328
|
await fs.ensureDir(path.dirname(finalOutputHtmlPath));
|
|
388
329
|
await fs.writeFile(finalOutputHtmlPath, pageHtml);
|
|
389
330
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
331
|
+
const sitemapOutputPath = isIndexFile
|
|
332
|
+
? (path.dirname(relativePath) === '.' ? '' : path.dirname(relativePath) + '/')
|
|
333
|
+
: relativePath.replace(/\.md$/, '/');
|
|
334
|
+
|
|
335
|
+
processedPages.push({
|
|
336
|
+
outputPath: sitemapOutputPath.replace(/\\/g, '/'),
|
|
395
337
|
frontmatter: pageFrontmatter
|
|
396
|
-
};
|
|
397
|
-
processedPages.push(processedPage);
|
|
338
|
+
});
|
|
398
339
|
} catch (error) {
|
|
399
|
-
console.error(
|
|
340
|
+
console.error(`❌ An unexpected error occurred while processing file ${path.relative(CWD, filePath)}:`, error);
|
|
400
341
|
}
|
|
401
342
|
}
|
|
402
343
|
|
package/src/commands/dev.js
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Source file from the docmd project — https://github.com/mgks/docmd
|
|
2
|
+
|
|
1
3
|
const fs = require('fs-extra');
|
|
2
4
|
const path = require('path');
|
|
3
5
|
const readline = require('readline');
|
|
@@ -33,7 +35,8 @@ module.exports = {
|
|
|
33
35
|
name: 'sky', // Themes: 'default', 'sky'
|
|
34
36
|
defaultMode: 'light', // Initial color mode: 'light' or 'dark'
|
|
35
37
|
enableModeToggle: true, // Show UI button to toggle light/dark modes
|
|
36
|
-
positionMode: '
|
|
38
|
+
positionMode: 'top', // 'top' or 'bottom' for the theme toggle
|
|
39
|
+
codeHighlight: true, // Enable/disable codeblock highlighting and import of highlight.js
|
|
37
40
|
customCss: [ // Array of paths to custom CSS files
|
|
38
41
|
// '/assets/css/custom.css', // Custom TOC styles
|
|
39
42
|
]
|
|
@@ -47,6 +50,7 @@ module.exports = {
|
|
|
47
50
|
|
|
48
51
|
// Content Processing
|
|
49
52
|
autoTitleFromH1: true, // Set to true to automatically use the first H1 as page title
|
|
53
|
+
copyCode: true, // Enable/disable the copy code button on code blocks
|
|
50
54
|
|
|
51
55
|
// Plugins Configuration
|
|
52
56
|
// Plugins are configured here. docmd will look for these keys.
|
|
@@ -92,6 +96,7 @@ module.exports = {
|
|
|
92
96
|
title: 'Getting Started',
|
|
93
97
|
icon: 'rocket',
|
|
94
98
|
path: '#',
|
|
99
|
+
collapsible: true, // This makes the menu section collapsible
|
|
95
100
|
children: [
|
|
96
101
|
{ title: 'Documentation', path: 'https://docmd.mgks.dev', icon: 'scroll', external: true },
|
|
97
102
|
{ title: 'Installation', path: 'https://docmd.mgks.dev/getting-started/installation', icon: 'download', external: true },
|