@mgks/docmd 0.1.2 → 0.1.3

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.
@@ -8,6 +8,25 @@ const fs = require('fs-extra');
8
8
  const { buildSite } = require('./build'); // Re-use the build logic
9
9
  const { loadConfig } = require('../core/config-loader');
10
10
 
11
+ /**
12
+ * Format paths for display to make them relative to CWD
13
+ * @param {string} absolutePath - The absolute path to format
14
+ * @param {string} cwd - Current working directory
15
+ * @returns {string} - Formatted relative path
16
+ */
17
+ function formatPathForDisplay(absolutePath, cwd) {
18
+ // Get the relative path from CWD
19
+ const relativePath = path.relative(cwd, absolutePath);
20
+
21
+ // If it's not a subdirectory, prefix with ./ for clarity
22
+ if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
23
+ return `./${relativePath}`;
24
+ }
25
+
26
+ // Return the relative path
27
+ return relativePath;
28
+ }
29
+
11
30
  async function startDevServer(configPathOption, options = { preserve: false }) {
12
31
  let config = await loadConfig(configPathOption); // Load initial config
13
32
  const CWD = process.cwd(); // Current Working Directory where user runs `docmd dev`
@@ -48,22 +67,24 @@ async function startDevServer(configPathOption, options = { preserve: false }) {
48
67
  console.error('WebSocket Server error:', error);
49
68
  });
50
69
 
51
-
52
70
  function broadcastReload() {
53
- // console.log('Broadcasting reload to', wsClients.size, 'clients');
54
71
  wsClients.forEach(client => {
55
72
  if (client.readyState === WebSocket.OPEN) {
56
- client.send('reload');
73
+ try {
74
+ client.send('reload');
75
+ } catch (error) {
76
+ console.error('Error sending reload command to client:', error);
77
+ }
57
78
  }
58
79
  });
59
80
  }
60
81
 
61
- // Inject live reload script into HTML
82
+ // Inject live reload script into HTML responses
62
83
  app.use((req, res, next) => {
63
- if (req.path.endsWith('.html')) {
84
+ if (req.path.endsWith('.html') || !req.path.includes('.')) {
64
85
  const originalSend = res.send;
65
- res.send = function (body) {
66
- if (typeof body === 'string') {
86
+ res.send = function(body) {
87
+ if (typeof body === 'string' && body.includes('</body>')) {
67
88
  const liveReloadScript = `
68
89
  <script>
69
90
  (function() {
@@ -156,7 +177,7 @@ async function startDevServer(configPathOption, options = { preserve: false }) {
156
177
  // Initial build
157
178
  console.log('🚀 Performing initial build for dev server...');
158
179
  try {
159
- await buildSite(configPathOption, { isDev: true, preserve: options.preserve }); // Use the original config path option
180
+ await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true }); // Use the original config path option
160
181
  console.log('✅ Initial build complete.');
161
182
  } catch (error) {
162
183
  console.error('❌ Initial build failed:', error.message, error.stack);
@@ -186,14 +207,14 @@ async function startDevServer(configPathOption, options = { preserve: false }) {
186
207
  }
187
208
 
188
209
  console.log(`👀 Watching for changes in:`);
189
- console.log(` - Source: ${paths.srcDirToWatch}`);
190
- console.log(` - Config: ${paths.configFileToWatch}`);
210
+ console.log(` - Source: ${formatPathForDisplay(paths.srcDirToWatch, CWD)}`);
211
+ console.log(` - Config: ${formatPathForDisplay(paths.configFileToWatch, CWD)}`);
191
212
  if (userAssetsDirExists) {
192
- console.log(` - Assets: ${paths.userAssetsDir}`);
213
+ console.log(` - Assets: ${formatPathForDisplay(paths.userAssetsDir, CWD)}`);
193
214
  }
194
215
  if (process.env.DOCMD_DEV === 'true') {
195
- console.log(` - docmd Templates: ${DOCMD_TEMPLATES_DIR} (internal)`);
196
- console.log(` - docmd Assets: ${DOCMD_ASSETS_DIR} (internal)`);
216
+ console.log(` - docmd Templates: ${formatPathForDisplay(DOCMD_TEMPLATES_DIR, CWD)} (internal)`);
217
+ console.log(` - docmd Assets: ${formatPathForDisplay(DOCMD_ASSETS_DIR, CWD)} (internal)`);
197
218
  }
198
219
 
199
220
  const watcher = chokidar.watch(watchedPaths, {
@@ -219,7 +240,7 @@ async function startDevServer(configPathOption, options = { preserve: false }) {
219
240
  // For simplicity, we might need to restart the watcher or inform user to restart dev server if srcDir/outputDir change.
220
241
  // For now, we'll at least update the static server path.
221
242
  if (newPaths.outputDir !== paths.outputDir) {
222
- console.log(`Output directory changed from ${paths.outputDir} to ${newPaths.outputDir}. Updating static server.`);
243
+ console.log(`Output directory changed from ${formatPathForDisplay(paths.outputDir, CWD)} to ${formatPathForDisplay(newPaths.outputDir, CWD)}. Updating static server.`);
223
244
  staticMiddleware = express.static(newPaths.outputDir);
224
245
  }
225
246
  // If srcDirToWatch changes, chokidar won't automatically pick it up.
@@ -228,9 +249,9 @@ async function startDevServer(configPathOption, options = { preserve: false }) {
228
249
  paths = newPaths; // Update paths for next build reference
229
250
  }
230
251
 
231
- await buildSite(configPathOption, { isDev: true, preserve: options.preserve }); // Re-build using the potentially updated config path
252
+ await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true }); // Re-build using the potentially updated config path
232
253
  broadcastReload();
233
- console.log('✅ Rebuild complete. Browser will refresh automatically.');
254
+ console.log('✅ Rebuild complete.');
234
255
  } catch (error) {
235
256
  console.error('❌ Rebuild failed:', error.message, error.stack);
236
257
  }
@@ -238,19 +259,40 @@ async function startDevServer(configPathOption, options = { preserve: false }) {
238
259
 
239
260
  watcher.on('error', error => console.error(`Watcher error: ${error}`));
240
261
 
262
+ // Try different ports if the default port is in use
241
263
  const PORT = process.env.PORT || 3000;
242
- server.listen(PORT, async () => {
243
- // Check if index.html exists after initial build
244
- const indexHtmlPath = path.join(paths.outputDir, 'index.html');
245
- if (!await fs.pathExists(indexHtmlPath)) {
246
- console.warn(`⚠️ Warning: ${indexHtmlPath} not found after initial build.
247
- The dev server is running, but you might see a 404 for the root page.
248
- Ensure your '${config.srcDir}' directory contains an 'index.md' or your navigation points to existing files.`);
249
- }
250
- console.log(`🎉 Dev server started at http://localhost:${PORT}`);
251
- console.log(`Serving content from: ${paths.outputDir}`);
252
- console.log(`Live reload is active. Browser will refresh automatically when files change.`);
253
- });
264
+ const MAX_PORT_ATTEMPTS = 10;
265
+ let currentPort = PORT;
266
+
267
+ // Function to try starting the server on different ports
268
+ function tryStartServer(port, attempt = 1) {
269
+ server.listen(port)
270
+ .on('listening', async () => {
271
+ // Check if index.html exists after initial build
272
+ const indexHtmlPath = path.join(paths.outputDir, 'index.html');
273
+ if (!await fs.pathExists(indexHtmlPath)) {
274
+ console.warn(`⚠️ Warning: ${formatPathForDisplay(indexHtmlPath, CWD)} not found after initial build.
275
+ The dev server is running, but you might see a 404 for the root page.
276
+ Ensure your '${config.srcDir}' directory contains an 'index.md' or your navigation points to existing files.`);
277
+ }
278
+ console.log(`🎉 Dev server started at http://localhost:${port}`);
279
+ console.log(`Serving content from: ${formatPathForDisplay(paths.outputDir, CWD)}`);
280
+ console.log(`Live reload is active. Browser will refresh automatically when files change.`);
281
+ })
282
+ .on('error', (err) => {
283
+ if (err.code === 'EADDRINUSE' && attempt < MAX_PORT_ATTEMPTS) {
284
+ console.log(`Port ${port} is in use, trying port ${port + 1}...`);
285
+ server.close();
286
+ tryStartServer(port + 1, attempt + 1);
287
+ } else {
288
+ console.error(`Failed to start server: ${err.message}`);
289
+ process.exit(1);
290
+ }
291
+ });
292
+ }
293
+
294
+ // Start the server with port fallback
295
+ tryStartServer(currentPort);
254
296
 
255
297
  // Graceful shutdown
256
298
  process.on('SIGINT', () => {
@@ -5,6 +5,21 @@ const matter = require('gray-matter');
5
5
  const hljs = require('highlight.js');
6
6
  const container = require('markdown-it-container');
7
7
  const attrs = require('markdown-it-attrs');
8
+ const path = require('path'); // Add path module for findMarkdownFiles
9
+
10
+ // Function to format paths for display (relative to CWD)
11
+ function formatPathForDisplay(absolutePath) {
12
+ const CWD = process.cwd();
13
+ const relativePath = path.relative(CWD, absolutePath);
14
+
15
+ // If it's not a subdirectory, prefix with ./ for clarity
16
+ if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
17
+ return `./${relativePath}`;
18
+ }
19
+
20
+ // Return the relative path
21
+ return relativePath;
22
+ }
8
23
 
9
24
  const md = new MarkdownIt({
10
25
  html: true,
@@ -335,7 +350,7 @@ function extractHeadingsFromHtml(htmlContent) {
335
350
  return headings;
336
351
  }
337
352
 
338
- async function processMarkdownFile(filePath) {
353
+ async function processMarkdownFile(filePath, options = { isDev: false }) {
339
354
  const rawContent = await fs.readFile(filePath, 'utf8');
340
355
  let frontmatter, markdownContent;
341
356
 
@@ -346,21 +361,43 @@ async function processMarkdownFile(filePath) {
346
361
  } catch (e) {
347
362
  if (e.name === 'YAMLException') {
348
363
  // Provide more specific error for YAML parsing issues
349
- const errorMessage = `Error parsing YAML frontmatter in ${filePath}: ${e.reason || e.message}${e.mark ? ` at line ${e.mark.line + 1}, column ${e.mark.column + 1}` : ''}. Please check the syntax.`;
364
+ const errorMessage = `Error parsing YAML frontmatter in ${formatPathForDisplay(filePath)}: ${e.reason || e.message}${e.mark ? ` at line ${e.mark.line + 1}, column ${e.mark.column + 1}` : ''}. Please check the syntax.`;
350
365
  console.error(`❌ ${errorMessage}`);
351
366
  throw new Error(errorMessage); // Propagate error to stop build/dev
352
367
  }
353
368
  // For other errors from gray-matter or unknown errors
354
- console.error(`❌ Error processing frontmatter in ${filePath}: ${e.message}`);
369
+ console.error(`❌ Error processing frontmatter in ${formatPathForDisplay(filePath)}: ${e.message}`);
355
370
  throw e;
356
371
  }
357
372
 
358
373
  if (!frontmatter.title) {
359
- console.warn(`⚠️ Warning: Markdown file ${filePath} is missing a 'title' in its frontmatter. Using filename as fallback.`);
374
+ console.warn(`⚠️ Warning: Markdown file ${formatPathForDisplay(filePath)} is missing a 'title' in its frontmatter. Using filename as fallback.`);
360
375
  // Fallback title, or you could make it an error
361
376
  // frontmatter.title = path.basename(filePath, path.extname(filePath));
362
377
  }
363
378
 
379
+ // Special handling for no-style pages with HTML content
380
+ if (frontmatter.noStyle === true) {
381
+ // Only log when not in dev mode to reduce console output during dev
382
+ if (!options.isDev) {
383
+ console.log(`📄 Processing no-style page: ${formatPathForDisplay(filePath)} - Using raw HTML content`);
384
+ }
385
+
386
+ // For no-style pages, we'll use the raw content directly
387
+ // No markdown processing, no HTML escaping
388
+ const htmlContent = markdownContent;
389
+
390
+ // Extract headings for table of contents (if needed)
391
+ const headings = extractHeadingsFromHtml(htmlContent);
392
+
393
+ return {
394
+ frontmatter,
395
+ htmlContent,
396
+ headings,
397
+ };
398
+ }
399
+
400
+ // Regular processing for standard pages
364
401
  // Check if this is a documentation example showing how to use containers
365
402
  const isContainerDocumentation = markdownContent.includes('containerName [optionalTitleOrType]') ||
366
403
  markdownContent.includes('## Callouts') ||
@@ -413,4 +450,29 @@ async function processMarkdownFile(filePath) {
413
450
  };
414
451
  }
415
452
 
416
- module.exports = { processMarkdownFile, mdInstance: md, extractHeadingsFromHtml }; // Export mdInstance if needed by plugins for consistency
453
+ // Add findMarkdownFiles function
454
+ /**
455
+ * Recursively finds all Markdown files in a directory and its subdirectories
456
+ * @param {string} dir - Directory to search in
457
+ * @returns {Promise<string[]>} - Array of file paths
458
+ */
459
+ async function findMarkdownFiles(dir) {
460
+ let files = [];
461
+ const items = await fs.readdir(dir, { withFileTypes: true });
462
+ for (const item of items) {
463
+ const fullPath = path.join(dir, item.name);
464
+ if (item.isDirectory()) {
465
+ files = files.concat(await findMarkdownFiles(fullPath));
466
+ } else if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.markdown'))) {
467
+ files.push(fullPath);
468
+ }
469
+ }
470
+ return files;
471
+ }
472
+
473
+ module.exports = {
474
+ processMarkdownFile,
475
+ mdInstance: md,
476
+ extractHeadingsFromHtml,
477
+ findMarkdownFiles // Export the findMarkdownFiles function
478
+ };
@@ -18,7 +18,7 @@ async function processPluginHooks(config, pageData, relativePathToRoot) {
18
18
  // 1. Favicon (built-in handling)
19
19
  if (config.favicon) {
20
20
  const faviconPath = config.favicon.startsWith('/') ? config.favicon.substring(1) : config.favicon;
21
- faviconLinkHtml = ` <link rel="icon" href="${relativePathToRoot}${faviconPath}">\n`;
21
+ faviconLinkHtml = `<link rel="shortcut icon" href="${relativePathToRoot}${faviconPath}" type="image/x-icon">\n`;
22
22
  }
23
23
 
24
24
  // 2. Theme CSS (built-in handling for theme.name)
@@ -75,9 +75,21 @@ async function generateHtmlPage(templateData) {
75
75
  footerHtml = mdInstance.renderInline(config.footer);
76
76
  }
77
77
 
78
- const layoutTemplatePath = path.join(__dirname, '..', 'templates', 'layout.ejs');
78
+ // Determine which template to use based on frontmatter
79
+ let templateName = 'layout.ejs';
80
+ if (frontmatter.noStyle === true) {
81
+ templateName = 'no-style.ejs';
82
+
83
+ // For no-style pages, ensure we're passing the raw HTML content
84
+ // without any additional processing or escaping
85
+ if (content.includes('&lt;') || content.includes('&gt;')) {
86
+ console.warn(`⚠️ Warning: HTML content in no-style page appears to be escaped. This may cause rendering issues.`);
87
+ }
88
+ }
89
+
90
+ const layoutTemplatePath = path.join(__dirname, '..', 'templates', templateName);
79
91
  if (!await fs.pathExists(layoutTemplatePath)) {
80
- throw new Error(`Layout template not found: ${layoutTemplatePath}`);
92
+ throw new Error(`Template not found: ${layoutTemplatePath}`);
81
93
  }
82
94
  const layoutTemplate = await fs.readFile(layoutTemplatePath, 'utf8');
83
95
 
@@ -105,6 +117,7 @@ async function generateHtmlPage(templateData) {
105
117
  currentPagePath, // Pass the current page path for active state detection
106
118
  headings: headings || [], // Pass headings for TOC, default to empty array if not provided
107
119
  isActivePage, // Flag to determine if TOC should be shown
120
+ frontmatter, // Pass the entire frontmatter for no-style template
108
121
  ...pluginOutputs, // Spread all plugin generated HTML strings
109
122
  };
110
123
 
@@ -1,6 +1,20 @@
1
1
  const fs = require('fs-extra');
2
2
  const path = require('path');
3
3
 
4
+ // Function to format paths for display (relative to CWD)
5
+ function formatPathForDisplay(absolutePath) {
6
+ const CWD = process.cwd();
7
+ const relativePath = path.relative(CWD, absolutePath);
8
+
9
+ // If it's not a subdirectory, prefix with ./ for clarity
10
+ if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
11
+ return `./${relativePath}`;
12
+ }
13
+
14
+ // Return the relative path
15
+ return relativePath;
16
+ }
17
+
4
18
  /**
5
19
  * Generate sitemap.xml in the output directory root
6
20
  * @param {Object} config - The full configuration object
@@ -100,7 +114,7 @@ async function generateSitemap(config, pages, outputDir, options = { isDev: fals
100
114
 
101
115
  // Only show sitemap generation message in production mode or if DOCMD_DEV is true
102
116
  if (!options.isDev || process.env.DOCMD_DEV === 'true') {
103
- console.log(`✅ Generated sitemap at ${sitemapPath}`);
117
+ console.log(`✅ Generated sitemap at ${formatPathForDisplay(sitemapPath)}`);
104
118
  }
105
119
  }
106
120
 
@@ -0,0 +1,159 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+
7
+ <% if (frontmatter.components?.meta !== false) { %>
8
+ <%- metaTagsHtml || '' %>
9
+ <title><%= pageTitle %><% if (frontmatter.components?.siteTitle !== false) { %> | <%= siteTitle %><% } %></title>
10
+ <% if (description && !(metaTagsHtml && metaTagsHtml.includes('name="description"'))) { %>
11
+ <meta name="description" content="<%= description %>">
12
+ <% } %>
13
+ <% } %>
14
+
15
+ <% if (frontmatter.components?.favicon !== false) { %>
16
+ <%- faviconLinkHtml || '' %>
17
+ <% } %>
18
+
19
+ <% if (frontmatter.components?.css !== false) { %>
20
+ <link rel="stylesheet" href="<%= relativePathToRoot %>assets/css/docmd-main.css">
21
+ <% if (frontmatter.components?.highlight !== false) { %>
22
+ <link rel="stylesheet" href="<%= relativePathToRoot %>assets/css/docmd-highlight-<%= defaultMode === 'dark' ? 'dark' : 'light' %>.css" id="highlight-theme">
23
+ <% } %>
24
+ <% } %>
25
+
26
+ <% if (frontmatter.components?.theme !== false) { %>
27
+ <%- themeCssLinkHtml || '' %>
28
+ <% } %>
29
+
30
+ <% if (frontmatter.components?.customCss !== false && customCssFiles && customCssFiles.length > 0) { %>
31
+ <% customCssFiles.forEach(cssFile => { %>
32
+ <link rel="stylesheet" href="<%= relativePathToRoot %><%- cssFile.startsWith('/') ? cssFile.substring(1) : cssFile %>">
33
+ <% }); %>
34
+ <% } %>
35
+
36
+ <% if (frontmatter.components?.pluginStyles !== false) { %>
37
+ <%- pluginStylesHtml || '' %>
38
+ <% } %>
39
+
40
+ <% if (frontmatter.components?.pluginHeadScripts !== false) { %>
41
+ <%- pluginHeadScriptsHtml || '' %>
42
+ <% } %>
43
+
44
+ <% if (frontmatter.customHead) { %>
45
+ <%- frontmatter.customHead %>
46
+ <% } %>
47
+ </head>
48
+ <body<% if (frontmatter.components?.theme !== false) { %> data-theme="<%= defaultMode %>"<% } %><% if (frontmatter.bodyClass) { %> class="<%= frontmatter.bodyClass %>"<% } %>>
49
+ <% if (frontmatter.components?.layout === true || frontmatter.components?.layout === 'full') { %>
50
+ <div class="main-content-wrapper">
51
+ <% if (frontmatter.components?.header !== false) { %>
52
+ <header class="page-header">
53
+ <% if (frontmatter.components?.pageTitle !== false) { %>
54
+ <h1><%= pageTitle %></h1>
55
+ <% } %>
56
+ </header>
57
+ <% } %>
58
+ <main class="content-area">
59
+ <div class="content-layout">
60
+ <div class="main-content">
61
+ <%- content %>
62
+ </div>
63
+ </div>
64
+ </main>
65
+ <% if (frontmatter.components?.footer !== false) { %>
66
+ <footer class="page-footer">
67
+ <div class="footer-content">
68
+ <div class="user-footer">
69
+ <%- footerHtml || '' %>
70
+ </div>
71
+ <% if (frontmatter.components?.branding !== false) { %>
72
+ <div class="branding-footer">
73
+ 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>
74
+ </div>
75
+ <% } %>
76
+ </div>
77
+ </footer>
78
+ <% } %>
79
+ </div>
80
+ <% } else if (frontmatter.components?.sidebar === true) { %>
81
+ <aside class="sidebar">
82
+ <% if (frontmatter.components?.logo !== false && logo && logo.light && logo.dark) { %>
83
+ <div class="sidebar-header">
84
+ <a href="<%= logo.href || (relativePathToRoot + 'index.html') %>" class="logo-link">
85
+ <img src="<%= relativePathToRoot %><%- logo.light.startsWith('/') ? logo.light.substring(1) : logo.light %>" alt="<%= logo.alt || siteTitle %>" class="logo-light" <% if (logo.height) { %>style="height: <%= logo.height %>;"<% } %>>
86
+ <img src="<%= relativePathToRoot %><%- logo.dark.startsWith('/') ? logo.dark.substring(1) : logo.dark %>" alt="<%= logo.alt || siteTitle %>" class="logo-dark" <% if (logo.height) { %>style="height: <%= logo.height %>;"<% } %>>
87
+ </a>
88
+ </div>
89
+ <% } %>
90
+ <% if (frontmatter.components?.navigation !== false) { %>
91
+ <%- navigationHtml %>
92
+ <% } %>
93
+ <% if (frontmatter.components?.themeToggle !== false && theme && theme.enableModeToggle) { %>
94
+ <button id="theme-toggle-button" aria-label="Toggle theme" class="theme-toggle-button">
95
+ <%- renderIcon('sun', { class: 'icon-sun' }) %>
96
+ <%- renderIcon('moon', { class: 'icon-moon' }) %>
97
+ </button>
98
+ <% } %>
99
+ </aside>
100
+ <div class="main-content-wrapper">
101
+ <% if (frontmatter.components?.header !== false) { %>
102
+ <header class="page-header">
103
+ <% if (frontmatter.components?.pageTitle !== false) { %>
104
+ <h1><%= pageTitle %></h1>
105
+ <% } %>
106
+ </header>
107
+ <% } %>
108
+ <main class="content-area">
109
+ <div class="content-layout">
110
+ <div class="main-content">
111
+ <%- content %>
112
+ </div>
113
+ <% if (frontmatter.components?.toc !== false && headings && headings.length > 0) { %>
114
+ <div class="toc-sidebar">
115
+ <%- include('toc', { content, headings, navigationHtml, isActivePage }) %>
116
+ </div>
117
+ <% } %>
118
+ </div>
119
+ </main>
120
+ <% if (frontmatter.components?.footer !== false) { %>
121
+ <footer class="page-footer">
122
+ <div class="footer-content">
123
+ <div class="user-footer">
124
+ <%- footerHtml || '' %>
125
+ </div>
126
+ <% if (frontmatter.components?.branding !== false) { %>
127
+ <div class="branding-footer">
128
+ 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>
129
+ </div>
130
+ <% } %>
131
+ </div>
132
+ </footer>
133
+ <% } %>
134
+ </div>
135
+ <% } else { %>
136
+ <%- content %>
137
+ <% } %>
138
+
139
+ <% if (frontmatter.components?.scripts !== false) { %>
140
+ <% if (frontmatter.components?.themeToggle !== false) { %>
141
+ <script src="<%= relativePathToRoot %>assets/js/docmd-theme-toggle.js"></script>
142
+ <% } %>
143
+
144
+ <% if (frontmatter.components?.customJs !== false && customJsFiles && customJsFiles.length > 0) { %>
145
+ <% customJsFiles.forEach(jsFile => { %>
146
+ <script src="<%= relativePathToRoot %><%- jsFile.startsWith('/') ? jsFile.substring(1) : jsFile %>"></script>
147
+ <% }); %>
148
+ <% } %>
149
+
150
+ <% if (frontmatter.components?.pluginBodyScripts !== false) { %>
151
+ <%- pluginBodyScriptsHtml || '' %>
152
+ <% } %>
153
+ <% } %>
154
+
155
+ <% if (frontmatter.customScripts) { %>
156
+ <%- frontmatter.customScripts %>
157
+ <% } %>
158
+ </body>
159
+ </html>