@mgks/docmd 0.1.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.
- package/.gitattributes +2 -0
- package/.github/FUNDING.yml +15 -0
- package/.github/workflows/deploy-docmd.yml +45 -0
- package/.github/workflows/publish.yml +84 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/bin/docmd.js +63 -0
- package/config.js +137 -0
- package/docs/cli-commands.md +87 -0
- package/docs/configuration.md +166 -0
- package/docs/contributing.md +86 -0
- package/docs/deployment.md +129 -0
- package/docs/getting-started/basic-usage.md +88 -0
- package/docs/getting-started/index.md +21 -0
- package/docs/getting-started/installation.md +75 -0
- package/docs/index.md +56 -0
- package/docs/plugins/analytics.md +76 -0
- package/docs/plugins/index.md +71 -0
- package/docs/plugins/seo.md +79 -0
- package/docs/plugins/sitemap.md +88 -0
- package/docs/theming/available-themes.md +85 -0
- package/docs/theming/custom-css-js.md +84 -0
- package/docs/theming/icons.md +93 -0
- package/docs/theming/index.md +19 -0
- package/docs/theming/light-dark-mode.md +107 -0
- package/docs/writing-content/custom-containers.md +129 -0
- package/docs/writing-content/frontmatter.md +76 -0
- package/docs/writing-content/index.md +17 -0
- package/docs/writing-content/markdown-syntax.md +277 -0
- package/package.json +56 -0
- package/src/assets/css/highlight-dark.css +1 -0
- package/src/assets/css/highlight-light.css +1 -0
- package/src/assets/css/main.css +562 -0
- package/src/assets/css/theme-sky.css +499 -0
- package/src/assets/css/toc.css +76 -0
- package/src/assets/favicon.ico +0 -0
- package/src/assets/images/docmd-logo.png +0 -0
- package/src/assets/images/docmd-preview.png +0 -0
- package/src/assets/images/logo-dark.png +0 -0
- package/src/assets/images/logo-light.png +0 -0
- package/src/assets/js/theme-toggle.js +59 -0
- package/src/commands/build.js +300 -0
- package/src/commands/dev.js +182 -0
- package/src/commands/init.js +51 -0
- package/src/core/config-loader.js +28 -0
- package/src/core/file-processor.js +376 -0
- package/src/core/html-generator.js +139 -0
- package/src/core/icon-renderer.js +105 -0
- package/src/plugins/analytics.js +44 -0
- package/src/plugins/seo.js +65 -0
- package/src/plugins/sitemap.js +100 -0
- package/src/templates/layout.ejs +174 -0
- package/src/templates/navigation.ejs +107 -0
- package/src/templates/toc.ejs +34 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// src/plugins/seo.js
|
|
2
|
+
function generateSeoMetaTags(config, pageData, relativePathToRoot) {
|
|
3
|
+
let metaTagsHtml = '';
|
|
4
|
+
const { frontmatter, outputPath } = pageData;
|
|
5
|
+
|
|
6
|
+
if (frontmatter.noindex) {
|
|
7
|
+
metaTagsHtml += ' <meta name="robots" content="noindex">\n';
|
|
8
|
+
return metaTagsHtml; // No other SEO tags if noindex
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const siteTitle = config.siteTitle;
|
|
12
|
+
const pageTitle = frontmatter.title || 'Untitled'; // Ensure pageTitle is always defined
|
|
13
|
+
const description = frontmatter.description || config.plugins?.seo?.defaultDescription || '';
|
|
14
|
+
|
|
15
|
+
// Construct pageUrl - ensure siteUrl in config has no trailing slash
|
|
16
|
+
const siteUrl = config.siteUrl ? config.siteUrl.replace(/\/$/, '') : '';
|
|
17
|
+
const pageSegment = outputPath.replace(/index\.html$/, '').replace(/\.html$/, '');
|
|
18
|
+
const pageUrl = `${siteUrl}${pageSegment.startsWith('/') ? pageSegment : '/' + pageSegment}`;
|
|
19
|
+
|
|
20
|
+
metaTagsHtml += ` <meta name="description" content="${description}">\n`;
|
|
21
|
+
|
|
22
|
+
// Open Graph
|
|
23
|
+
metaTagsHtml += ` <meta property="og:title" content="${pageTitle} | ${siteTitle}">\n`;
|
|
24
|
+
metaTagsHtml += ` <meta property="og:description" content="${description}">\n`;
|
|
25
|
+
metaTagsHtml += ` <meta property="og:url" content="${pageUrl}">\n`;
|
|
26
|
+
metaTagsHtml += ` <meta property="og:site_name" content="${config.plugins?.seo?.openGraph?.siteName || siteTitle}">\n`;
|
|
27
|
+
|
|
28
|
+
const ogImage = frontmatter.image || frontmatter.ogImage || config.plugins?.seo?.openGraph?.defaultImage;
|
|
29
|
+
if (ogImage) {
|
|
30
|
+
const ogImageUrl = ogImage.startsWith('http') ? ogImage : `${siteUrl}${ogImage.startsWith('/') ? ogImage : '/' + ogImage}`;
|
|
31
|
+
metaTagsHtml += ` <meta property="og:image" content="${ogImageUrl}">\n`;
|
|
32
|
+
}
|
|
33
|
+
metaTagsHtml += ` <meta property="og:type" content="${frontmatter.ogType || 'website'}">\n`;
|
|
34
|
+
|
|
35
|
+
// Twitter Card
|
|
36
|
+
const twitterCardType = frontmatter.twitterCard || config.plugins?.seo?.twitter?.cardType || 'summary';
|
|
37
|
+
metaTagsHtml += ` <meta name="twitter:card" content="${twitterCardType}">\n`;
|
|
38
|
+
if (config.plugins?.seo?.twitter?.siteUsername) {
|
|
39
|
+
metaTagsHtml += ` <meta name="twitter:site" content="${config.plugins.seo.twitter.siteUsername}">\n`;
|
|
40
|
+
}
|
|
41
|
+
const twitterCreator = frontmatter.twitterCreator || config.plugins?.seo?.twitter?.creatorUsername;
|
|
42
|
+
if (twitterCreator) {
|
|
43
|
+
metaTagsHtml += ` <meta name="twitter:creator" content="${twitterCreator}">\n`;
|
|
44
|
+
}
|
|
45
|
+
// Twitter title, description, image often fallback to OG tags if not explicitly set by Twitter.
|
|
46
|
+
// For explicitness:
|
|
47
|
+
metaTagsHtml += ` <meta name="twitter:title" content="${pageTitle} | ${siteTitle}">\n`;
|
|
48
|
+
metaTagsHtml += ` <meta name="twitter:description" content="${description}">\n`;
|
|
49
|
+
if (ogImage) { // Re-use ogImage for twitter:image if not specified differently
|
|
50
|
+
const twitterImageUrl = ogImage.startsWith('http') ? ogImage : `${siteUrl}${ogImage.startsWith('/') ? ogImage : '/' + ogImage}`;
|
|
51
|
+
metaTagsHtml += ` <meta name="twitter:image" content="${twitterImageUrl}">\n`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
// Keywords (optional, less impact nowadays)
|
|
56
|
+
if (frontmatter.keywords) {
|
|
57
|
+
const keywordsString = Array.isArray(frontmatter.keywords) ? frontmatter.keywords.join(', ') : frontmatter.keywords;
|
|
58
|
+
metaTagsHtml += ` <meta name="keywords" content="${keywordsString}">\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
return metaTagsHtml;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { generateSeoMetaTags };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate sitemap.xml in the output directory root
|
|
6
|
+
* @param {Object} config - The full configuration object
|
|
7
|
+
* @param {Array} pages - Array of page objects with data about each processed page
|
|
8
|
+
* @param {string} outputDir - Path to the output directory
|
|
9
|
+
*/
|
|
10
|
+
async function generateSitemap(config, pages, outputDir) {
|
|
11
|
+
// Skip if no siteUrl is defined (sitemap needs absolute URLs)
|
|
12
|
+
if (!config.siteUrl) {
|
|
13
|
+
console.warn('⚠️ No siteUrl defined in config. Skipping sitemap generation.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Normalize siteUrl to ensure it has no trailing slash
|
|
18
|
+
const siteUrl = config.siteUrl.replace(/\/$/, '');
|
|
19
|
+
|
|
20
|
+
// Sitemap XML header
|
|
21
|
+
let sitemapXml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
22
|
+
sitemapXml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
|
|
23
|
+
|
|
24
|
+
// Get default settings from config
|
|
25
|
+
const defaultChangefreq = config.plugins?.sitemap?.defaultChangefreq || 'weekly';
|
|
26
|
+
const defaultPriority = config.plugins?.sitemap?.defaultPriority || 0.8;
|
|
27
|
+
|
|
28
|
+
const rootPriority = config.plugins?.sitemap?.rootPriority || 1.0;
|
|
29
|
+
|
|
30
|
+
// Helper function to convert paths to URLs
|
|
31
|
+
function pathToUrl(pagePath) {
|
|
32
|
+
// Handle index.html
|
|
33
|
+
if (pagePath === 'index.html') {
|
|
34
|
+
return siteUrl + '/';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// For paths already using pretty URLs (ending with slash)
|
|
38
|
+
if (pagePath.endsWith('/')) {
|
|
39
|
+
return siteUrl + '/' + pagePath;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// For paths still using .html extension
|
|
43
|
+
// Convert to no-extension format
|
|
44
|
+
if (pagePath.endsWith('.html')) {
|
|
45
|
+
const pathWithoutExt = pagePath.substring(0, pagePath.length - 5);
|
|
46
|
+
if (pathWithoutExt === '') {
|
|
47
|
+
return siteUrl + '/';
|
|
48
|
+
} else {
|
|
49
|
+
return siteUrl + '/' + pathWithoutExt + '/';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Default case
|
|
54
|
+
return siteUrl + '/' + pagePath;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const page of pages) {
|
|
58
|
+
// Parse frontmatter for sitemap-specific overrides and metadata
|
|
59
|
+
const frontmatter = page.frontmatter || {};
|
|
60
|
+
const pagePath = page.outputPath || '';
|
|
61
|
+
|
|
62
|
+
// Skip if page is explicitly excluded from sitemap
|
|
63
|
+
if (frontmatter.sitemap === false) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Determine URL for this page
|
|
68
|
+
let url = pathToUrl(pagePath);
|
|
69
|
+
|
|
70
|
+
// Set priority
|
|
71
|
+
let priority = frontmatter.priority ||
|
|
72
|
+
(pagePath === 'index.html' ? rootPriority : defaultPriority);
|
|
73
|
+
|
|
74
|
+
// Set change frequency
|
|
75
|
+
const changefreq = frontmatter.changefreq || defaultChangefreq;
|
|
76
|
+
|
|
77
|
+
// Add page to sitemap
|
|
78
|
+
sitemapXml += ' <url>\n';
|
|
79
|
+
sitemapXml += ` <loc>${url}</loc>\n`;
|
|
80
|
+
|
|
81
|
+
// Add lastmod if available in frontmatter
|
|
82
|
+
if (frontmatter.lastmod) {
|
|
83
|
+
sitemapXml += ` <lastmod>${frontmatter.lastmod}</lastmod>\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
sitemapXml += ` <changefreq>${changefreq}</changefreq>\n`;
|
|
87
|
+
sitemapXml += ` <priority>${priority}</priority>\n`;
|
|
88
|
+
sitemapXml += ' </url>\n';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
sitemapXml += '</urlset>';
|
|
92
|
+
|
|
93
|
+
// Write sitemap file
|
|
94
|
+
const sitemapPath = path.join(outputDir, 'sitemap.xml');
|
|
95
|
+
await fs.writeFile(sitemapPath, sitemapXml);
|
|
96
|
+
|
|
97
|
+
console.log(`✅ Generated sitemap at ${sitemapPath}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { generateSitemap };
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
<%- metaTagsHtml || '' %> <%# SEO Plugin Meta Tags %>
|
|
8
|
+
|
|
9
|
+
<title><%= pageTitle %> | <%= siteTitle %></title>
|
|
10
|
+
<% if (description && !(metaTagsHtml && metaTagsHtml.includes('name="description"'))) { %>
|
|
11
|
+
<meta name="description" content="<%= description %>">
|
|
12
|
+
<% } %>
|
|
13
|
+
|
|
14
|
+
<%- faviconLinkHtml || '' %> <%# Favicon %>
|
|
15
|
+
|
|
16
|
+
<link rel="stylesheet" href="<%= relativePathToRoot %>assets/css/main.css">
|
|
17
|
+
|
|
18
|
+
<link rel="stylesheet" href="<%= relativePathToRoot %>assets/css/highlight-<%= defaultMode === 'dark' ? 'dark' : 'light' %>.css" id="highlight-theme">
|
|
19
|
+
|
|
20
|
+
<%- themeCssLinkHtml || '' %> <%# For theme.name specific CSS %>
|
|
21
|
+
|
|
22
|
+
<% (customCssFiles || []).forEach(cssFile => { %>
|
|
23
|
+
<link rel="stylesheet" href="<%= relativePathToRoot %><%- cssFile.startsWith('/') ? cssFile.substring(1) : cssFile %>">
|
|
24
|
+
<% }); %>
|
|
25
|
+
|
|
26
|
+
<%- pluginStylesHtml || '' %> <%# Plugin specific CSS %>
|
|
27
|
+
|
|
28
|
+
<%- pluginHeadScriptsHtml || '' %> <%# Plugin specific head scripts (e.g., Analytics) %>
|
|
29
|
+
</head>
|
|
30
|
+
<body data-theme="<%= defaultMode %>">
|
|
31
|
+
<aside class="sidebar">
|
|
32
|
+
<div class="sidebar-header">
|
|
33
|
+
<% if (logo && logo.light && logo.dark) { %>
|
|
34
|
+
<a href="<%= logo.href || (relativePathToRoot + 'index.html') %>" class="logo-link">
|
|
35
|
+
<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 %>;"<% } %>>
|
|
36
|
+
<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 %>;"<% } %>>
|
|
37
|
+
</a>
|
|
38
|
+
<% } else { %>
|
|
39
|
+
<h1><a href="<%= relativePathToRoot %>index.html"><%= siteTitle %></a></h1>
|
|
40
|
+
<% } %>
|
|
41
|
+
</div>
|
|
42
|
+
<%- navigationHtml %>
|
|
43
|
+
<% if (theme && theme.enableModeToggle) { %>
|
|
44
|
+
<button id="theme-toggle-button" aria-label="Toggle theme" class="theme-toggle-button">
|
|
45
|
+
<%# renderIcon is available in the global EJS scope from html-generator %>
|
|
46
|
+
<%- renderIcon('sun', { class: 'icon-sun' }) %>
|
|
47
|
+
<%- renderIcon('moon', { class: 'icon-moon' }) %>
|
|
48
|
+
</button>
|
|
49
|
+
<% } %>
|
|
50
|
+
</aside>
|
|
51
|
+
<div class="main-content-wrapper">
|
|
52
|
+
<header class="page-header">
|
|
53
|
+
<h1><%= pageTitle %></h1>
|
|
54
|
+
</header>
|
|
55
|
+
<main class="content-area">
|
|
56
|
+
<div class="content-layout">
|
|
57
|
+
<div class="main-content">
|
|
58
|
+
<%- content %>
|
|
59
|
+
|
|
60
|
+
<% if (prevPage || nextPage) { %>
|
|
61
|
+
<div class="page-navigation">
|
|
62
|
+
<% if (prevPage) { %>
|
|
63
|
+
<a href="<%= prevPage.url %>" class="prev-page">
|
|
64
|
+
<%- renderIcon('arrow-left', { class: 'page-nav-icon' }) %>
|
|
65
|
+
<span>
|
|
66
|
+
<small>Previous</small>
|
|
67
|
+
<strong><%= prevPage.title %></strong>
|
|
68
|
+
</span>
|
|
69
|
+
</a>
|
|
70
|
+
<% } else { %>
|
|
71
|
+
<div class="prev-page-placeholder"></div>
|
|
72
|
+
<% } %>
|
|
73
|
+
|
|
74
|
+
<% if (nextPage) { %>
|
|
75
|
+
<a href="<%= nextPage.url %>" class="next-page">
|
|
76
|
+
<span>
|
|
77
|
+
<small>Next</small>
|
|
78
|
+
<strong><%= nextPage.title %></strong>
|
|
79
|
+
</span>
|
|
80
|
+
<%- renderIcon('arrow-right', { class: 'page-nav-icon' }) %>
|
|
81
|
+
</a>
|
|
82
|
+
<% } else { %>
|
|
83
|
+
<div class="next-page-placeholder"></div>
|
|
84
|
+
<% } %>
|
|
85
|
+
</div>
|
|
86
|
+
<% } %>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- DEBUG: <%- JSON.stringify({headingsLength: headings ? headings.length : 0}) %> -->
|
|
90
|
+
|
|
91
|
+
<!-- TOC sidebar -->
|
|
92
|
+
<div class="toc-sidebar">
|
|
93
|
+
<%
|
|
94
|
+
// Helper function to decode HTML entities
|
|
95
|
+
function decodeHtmlEntities(html) {
|
|
96
|
+
return html
|
|
97
|
+
.replace(/&/g, '&')
|
|
98
|
+
.replace(/</g, '<')
|
|
99
|
+
.replace(/>/g, '>')
|
|
100
|
+
.replace(/"/g, '"')
|
|
101
|
+
.replace(/'/g, "'")
|
|
102
|
+
.replace(/ /g, ' ');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Only show TOC on active pages
|
|
106
|
+
const isActive = navigationHtml && navigationHtml.includes('class="active"');
|
|
107
|
+
|
|
108
|
+
if (isActive) {
|
|
109
|
+
// Extract headings directly from content - match with or without id attribute
|
|
110
|
+
const headingRegex = /<h([2-4])[^>]*?(?:id="([^"]*)")?[^>]*?>([\s\S]*?)<\/h\1>/g;
|
|
111
|
+
const tocHeadings = [];
|
|
112
|
+
let match;
|
|
113
|
+
let contentStr = content.toString();
|
|
114
|
+
|
|
115
|
+
while ((match = headingRegex.exec(contentStr)) !== null) {
|
|
116
|
+
const level = parseInt(match[1], 10);
|
|
117
|
+
// Use ID if available, or generate one from the text
|
|
118
|
+
let id = match[2];
|
|
119
|
+
// Remove any HTML tags inside the heading text and decode HTML entities
|
|
120
|
+
const textWithTags = match[3].replace(/<\/?[^>]+(>|$)/g, '');
|
|
121
|
+
// Decode HTML entities
|
|
122
|
+
const text = decodeHtmlEntities(textWithTags);
|
|
123
|
+
|
|
124
|
+
if (!id) {
|
|
125
|
+
// Generate an ID from the heading text if none exists
|
|
126
|
+
id = text
|
|
127
|
+
.toLowerCase()
|
|
128
|
+
.replace(/\s+/g, '-')
|
|
129
|
+
.replace(/[^\w-]/g, '')
|
|
130
|
+
.replace(/--+/g, '-')
|
|
131
|
+
.replace(/^-+|-+$/g, '');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
tocHeadings.push({ id, level, text });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Only show TOC if there are enough headings
|
|
138
|
+
if (tocHeadings.length > 1) {
|
|
139
|
+
%>
|
|
140
|
+
<div class="toc-container">
|
|
141
|
+
<h2 class="toc-title">On This Page</h2>
|
|
142
|
+
<ul class="toc-list">
|
|
143
|
+
<% tocHeadings.forEach(heading => { %>
|
|
144
|
+
<li class="toc-item toc-level-<%= heading.level %>">
|
|
145
|
+
<a href="#<%= heading.id %>" class="toc-link"><%- heading.text %></a>
|
|
146
|
+
</li>
|
|
147
|
+
<% }); %>
|
|
148
|
+
</ul>
|
|
149
|
+
</div>
|
|
150
|
+
<% }
|
|
151
|
+
} %>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</main>
|
|
155
|
+
<footer class="page-footer">
|
|
156
|
+
<div class="footer-content">
|
|
157
|
+
<div class="user-footer">
|
|
158
|
+
<%- footerHtml || '' %>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="branding-footer">
|
|
161
|
+
Build with 💜 <a href="https://docmd.mgks.dev" target="_blank" rel="noopener">docmd.</a>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</footer>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<script src="<%= relativePathToRoot %>assets/js/theme-toggle.js"></script>
|
|
168
|
+
<% (customJsFiles || []).forEach(jsFile => { %>
|
|
169
|
+
<script src="<%= relativePathToRoot %><%- jsFile.startsWith('/') ? jsFile.substring(1) : jsFile %>"></script>
|
|
170
|
+
<% }); %>
|
|
171
|
+
|
|
172
|
+
<%- pluginBodyScriptsHtml || '' %> <%# Plugin specific body scripts %>
|
|
173
|
+
</body>
|
|
174
|
+
</html>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<%# src/templates/navigation.ejs %>
|
|
2
|
+
|
|
3
|
+
<%# renderIcon is passed from html-generator.js %>
|
|
4
|
+
|
|
5
|
+
<%
|
|
6
|
+
// Debug function - uncomment to troubleshoot paths
|
|
7
|
+
function debugNavPaths(item, itemPath, currentPath, isActive, isParentActive) {
|
|
8
|
+
console.log('\nDEBUG NAV ITEM:');
|
|
9
|
+
console.log(`Title: ${item.title}`);
|
|
10
|
+
console.log(`Path: ${item.path}`);
|
|
11
|
+
console.log(`Computed item path: ${itemPath}`);
|
|
12
|
+
console.log(`Current page path: ${currentPath}`);
|
|
13
|
+
console.log(`Is directly active: ${isActive}`);
|
|
14
|
+
console.log(`Is parent active: ${isParentActive}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function renderNavItems(items, currentLevelPagePath, rootPath) { %>
|
|
18
|
+
<ul>
|
|
19
|
+
<% items.forEach(item => { %>
|
|
20
|
+
<%
|
|
21
|
+
let itemHref = '#'; // Default for non-linking parents or error
|
|
22
|
+
let isCurrentPageActive = false;
|
|
23
|
+
let isActivePage = false; // For the direct page itself
|
|
24
|
+
const isExternal = item.external === true;
|
|
25
|
+
const targetBlank = isExternal ? 'target="_blank" rel="noopener noreferrer"' : '';
|
|
26
|
+
let iconHtml = '';
|
|
27
|
+
let externalLinkIconHtml = '';
|
|
28
|
+
|
|
29
|
+
if (item.icon) {
|
|
30
|
+
// Use custom class for nav icons for specific styling
|
|
31
|
+
iconHtml = renderIcon(item.icon, { class: 'nav-item-icon' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (isExternal) {
|
|
35
|
+
itemHref = item.path; // Full URL for external links
|
|
36
|
+
// Use a Lucide icon like 'arrow-up-right' or 'external-link'
|
|
37
|
+
externalLinkIconHtml = renderIcon('arrow-up-right', { class: 'nav-external-icon', width: '0.8em', height: '0.8em' });
|
|
38
|
+
} else {
|
|
39
|
+
// Path normalization for internal links
|
|
40
|
+
let cleanPath = item.path.startsWith('/') ? item.path : '/' + item.path; // Ensure leading slash
|
|
41
|
+
|
|
42
|
+
// Handle paths with pretty URLs
|
|
43
|
+
if (cleanPath === '/') {
|
|
44
|
+
// Root path goes to index.html
|
|
45
|
+
itemHref = rootPath + 'index.html';
|
|
46
|
+
} else {
|
|
47
|
+
// Remove any trailing slash for consistency
|
|
48
|
+
const pathWithoutTrailingSlash = cleanPath.endsWith('/') ? cleanPath.slice(0, -1) : cleanPath;
|
|
49
|
+
|
|
50
|
+
// For all other paths, link to the directory (pretty URL)
|
|
51
|
+
// Remove leading slash and ensure clean path
|
|
52
|
+
const cleanedPath = pathWithoutTrailingSlash.substring(1);
|
|
53
|
+
itemHref = rootPath + cleanedPath + '/';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// SIMPLIFIED ACTIVE STATE LOGIC - START
|
|
57
|
+
// 1. Prepare the paths for comparison
|
|
58
|
+
// The item path in the format used in the navigation config
|
|
59
|
+
const normalizedItemPath = cleanPath.substring(1) || '';
|
|
60
|
+
|
|
61
|
+
// Special handling for root path
|
|
62
|
+
if (cleanPath === '/') {
|
|
63
|
+
isActivePage = currentLevelPagePath === 'index.html';
|
|
64
|
+
isCurrentPageActive = isActivePage;
|
|
65
|
+
}
|
|
66
|
+
// Parent folder with index.md
|
|
67
|
+
else if (cleanPath.endsWith('/')) {
|
|
68
|
+
// Direct match for folder/index.html pages
|
|
69
|
+
const folderPath = cleanPath.substring(1, cleanPath.length - 1); // Remove leading / and trailing /
|
|
70
|
+
isActivePage = currentLevelPagePath === folderPath + '/';
|
|
71
|
+
isCurrentPageActive = isActivePage;
|
|
72
|
+
|
|
73
|
+
// Check if any children are active
|
|
74
|
+
// If current path starts with this item's path, it's a parent of the active page
|
|
75
|
+
if (!isActivePage && currentLevelPagePath.startsWith(normalizedItemPath)) {
|
|
76
|
+
isCurrentPageActive = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Regular page
|
|
80
|
+
else {
|
|
81
|
+
const pagePath = cleanPath.substring(1) + '/';
|
|
82
|
+
isActivePage = currentLevelPagePath === pagePath;
|
|
83
|
+
isCurrentPageActive = isActivePage;
|
|
84
|
+
}
|
|
85
|
+
// SIMPLIFIED ACTIVE STATE LOGIC - END
|
|
86
|
+
}
|
|
87
|
+
%>
|
|
88
|
+
<li class="<%= isCurrentPageActive ? 'active-parent' : '' %>">
|
|
89
|
+
<a href="<%= itemHref %>"
|
|
90
|
+
class="<%= isActivePage ? 'active' : '' %>"
|
|
91
|
+
<%- targetBlank %>>
|
|
92
|
+
<% if (iconHtml) { %><%- iconHtml %><% } %>
|
|
93
|
+
<span><%- item.title %></span> <%# Wrap title in span for styling if needed %>
|
|
94
|
+
<% if (externalLinkIconHtml) { %><%- externalLinkIconHtml %><% } %>
|
|
95
|
+
</a>
|
|
96
|
+
|
|
97
|
+
<% if (item.children && item.children.length > 0) { %>
|
|
98
|
+
<%- renderNavItems(item.children, currentLevelPagePath, rootPath) %>
|
|
99
|
+
<% } %>
|
|
100
|
+
</li>
|
|
101
|
+
<% }); %>
|
|
102
|
+
</ul>
|
|
103
|
+
<% } %>
|
|
104
|
+
|
|
105
|
+
<nav class="sidebar-nav">
|
|
106
|
+
<%- renderNavItems(navItems, currentPagePath, relativePathToRoot) %>
|
|
107
|
+
</nav>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<%# src/templates/toc.ejs %>
|
|
2
|
+
<%
|
|
3
|
+
// If direct headings aren't available, we'll try to extract them from the content
|
|
4
|
+
let tocHeadings = [];
|
|
5
|
+
if (headings && headings.length > 0) {
|
|
6
|
+
// Use provided headings if available
|
|
7
|
+
tocHeadings = headings.filter(h => h.level >= 2 && h.level <= 4);
|
|
8
|
+
} else if (content) {
|
|
9
|
+
// Basic regex to extract headings from HTML content
|
|
10
|
+
const headingRegex = /<h([2-4])[^>]*?id="([^"]*)"[^>]*?>([\s\S]*?)<\/h\1>/g;
|
|
11
|
+
let match;
|
|
12
|
+
while ((match = headingRegex.exec(content)) !== null) {
|
|
13
|
+
const level = parseInt(match[1], 10);
|
|
14
|
+
const id = match[2];
|
|
15
|
+
// Remove any HTML tags inside the heading text
|
|
16
|
+
const text = match[3].replace(/<\/?[^>]+(>|$)/g, '');
|
|
17
|
+
tocHeadings.push({ id, level, text });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Only show TOC if there are enough headings
|
|
22
|
+
if (tocHeadings.length > 1) {
|
|
23
|
+
%>
|
|
24
|
+
<div class="toc-container">
|
|
25
|
+
<h2 class="toc-title">On This Page</h2>
|
|
26
|
+
<ul class="toc-list">
|
|
27
|
+
<% tocHeadings.forEach(heading => { %>
|
|
28
|
+
<li class="toc-item toc-level-<%= heading.level %>">
|
|
29
|
+
<a href="#<%= heading.id %>" class="toc-link"><%= heading.text %></a>
|
|
30
|
+
</li>
|
|
31
|
+
<% }); %>
|
|
32
|
+
</ul>
|
|
33
|
+
</div>
|
|
34
|
+
<% } %>
|