@mgks/docmd 0.3.8 → 0.3.9
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/package.json +2 -2
- package/src/assets/css/docmd-main.css +32 -3
- package/src/assets/js/docmd-main.js +38 -0
- package/src/core/config-loader.js +1 -0
- package/src/core/html-generator.js +40 -45
- package/src/core/markdown/rules.js +137 -89
- package/src/core/markdown/setup.js +8 -19
- package/src/core/navigation-helper.js +51 -39
- package/src/templates/partials/theme-init.js +12 -12
- package/src/core/html-formatter.js +0 -97
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mgks/docmd",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Generate beautiful, lightweight static documentation sites directly from your Markdown files. Zero clutter, just content.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"esbuild": "^0.27.2",
|
|
38
38
|
"gray-matter": "^4.0.3",
|
|
39
39
|
"highlight.js": "^11.11.1",
|
|
40
|
-
"lucide-static": "^0.
|
|
40
|
+
"lucide-static": "^0.563.0",
|
|
41
41
|
"markdown-it": "^14.1.0",
|
|
42
42
|
"markdown-it-abbr": "^2.0.0",
|
|
43
43
|
"markdown-it-attrs": "^4.3.1",
|
|
@@ -51,6 +51,11 @@
|
|
|
51
51
|
--lightbox-text: #fff
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
html {
|
|
55
|
+
scroll-behavior: smooth;
|
|
56
|
+
scroll-padding-top: 80px;
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
body {
|
|
55
60
|
font-family: var(--font-family-sans);
|
|
56
61
|
background-color: var(--bg-color);
|
|
@@ -212,7 +217,6 @@ body.sidebar-collapsed .main-content-wrapper {
|
|
|
212
217
|
flex-grow: 1;
|
|
213
218
|
display: flex;
|
|
214
219
|
flex-direction: column;
|
|
215
|
-
overflow: hidden
|
|
216
220
|
}
|
|
217
221
|
|
|
218
222
|
.header-left,
|
|
@@ -886,11 +890,36 @@ details[open]>.collapsible-summary .collapsible-arrow svg {
|
|
|
886
890
|
}
|
|
887
891
|
|
|
888
892
|
.toc-sidebar {
|
|
889
|
-
|
|
893
|
+
position: -webkit-sticky;
|
|
890
894
|
position: sticky;
|
|
891
895
|
top: 2rem;
|
|
896
|
+
max-height: calc(100vh - 15rem);
|
|
892
897
|
overflow-y: auto;
|
|
893
|
-
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.toc-link.active {
|
|
901
|
+
font-weight: 700;
|
|
902
|
+
color: var(--link-color);
|
|
903
|
+
border-left: 2px solid var(--link-color);
|
|
904
|
+
padding-left: 8px;
|
|
905
|
+
margin-left: -10px;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
@media (max-width: 1024px) {
|
|
909
|
+
.content-layout {
|
|
910
|
+
flex-direction: column-reverse;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.toc-sidebar {
|
|
914
|
+
position: static;
|
|
915
|
+
width: 100%;
|
|
916
|
+
max-height: none;
|
|
917
|
+
margin-bottom: 2rem;
|
|
918
|
+
border-left: none;
|
|
919
|
+
border-bottom: 1px solid var(--border-color);
|
|
920
|
+
padding-left: 0;
|
|
921
|
+
padding-bottom: 1rem;
|
|
922
|
+
}
|
|
894
923
|
}
|
|
895
924
|
|
|
896
925
|
.docmd-tabs,
|
|
@@ -209,6 +209,43 @@ function syncBodyTheme() {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
// --- Scroll Spy Logic ---
|
|
213
|
+
function initializeScrollSpy() {
|
|
214
|
+
const tocLinks = document.querySelectorAll('.toc-link');
|
|
215
|
+
const headings = document.querySelectorAll('.main-content h2, .main-content h3');
|
|
216
|
+
|
|
217
|
+
if (tocLinks.length === 0 || headings.length === 0) return;
|
|
218
|
+
|
|
219
|
+
const observerOptions = {
|
|
220
|
+
root: null,
|
|
221
|
+
// Trigger when heading crosses the top 10% of screen
|
|
222
|
+
rootMargin: '-10% 0px -80% 0px',
|
|
223
|
+
threshold: 0
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const observer = new IntersectionObserver((entries) => {
|
|
227
|
+
entries.forEach(entry => {
|
|
228
|
+
if (entry.isIntersecting) {
|
|
229
|
+
// 1. Clear current active state
|
|
230
|
+
tocLinks.forEach(link => link.classList.remove('active'));
|
|
231
|
+
|
|
232
|
+
// 2. Find link corresponding to this heading
|
|
233
|
+
const id = entry.target.getAttribute('id');
|
|
234
|
+
const activeLink = document.querySelector(`.toc-link[href="#${id}"]`);
|
|
235
|
+
|
|
236
|
+
if (activeLink) {
|
|
237
|
+
activeLink.classList.add('active');
|
|
238
|
+
|
|
239
|
+
// Optional: Auto-scroll the TOC sidebar itself if needed
|
|
240
|
+
// activeLink.scrollIntoView({ block: 'nearest' });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}, observerOptions);
|
|
245
|
+
|
|
246
|
+
headings.forEach(heading => observer.observe(heading));
|
|
247
|
+
}
|
|
248
|
+
|
|
212
249
|
// --- Main Execution ---
|
|
213
250
|
document.addEventListener('DOMContentLoaded', () => {
|
|
214
251
|
syncBodyTheme();
|
|
@@ -219,4 +256,5 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
219
256
|
initializeCollapsibleNav();
|
|
220
257
|
initializeMobileMenus();
|
|
221
258
|
initializeSidebarScroll();
|
|
259
|
+
initializeScrollSpy();
|
|
222
260
|
});
|
|
@@ -38,6 +38,7 @@ async function loadConfig(configPath) {
|
|
|
38
38
|
validateConfig(config);
|
|
39
39
|
|
|
40
40
|
// Basic validation and defaults
|
|
41
|
+
config.base = config.base || '/';
|
|
41
42
|
config.srcDir = config.srcDir || 'docs';
|
|
42
43
|
config.outputDir = config.outputDir || 'site';
|
|
43
44
|
config.theme = config.theme || {};
|
|
@@ -7,12 +7,10 @@ const { createMarkdownItInstance } = require('./file-processor');
|
|
|
7
7
|
const { generateSeoMetaTags } = require('../plugins/seo');
|
|
8
8
|
const { generateAnalyticsScripts } = require('../plugins/analytics');
|
|
9
9
|
const { renderIcon } = require('./icon-renderer');
|
|
10
|
-
const { formatHtml } = require('./html-formatter');
|
|
11
10
|
|
|
12
11
|
let mdInstance = null;
|
|
13
12
|
let themeInitScript = '';
|
|
14
13
|
|
|
15
|
-
// Load the theme initialization script into memory once to avoid repeated disk I/O
|
|
16
14
|
(async () => {
|
|
17
15
|
try {
|
|
18
16
|
const themeInitPath = path.join(__dirname, '..', 'templates', 'partials', 'theme-init.js');
|
|
@@ -23,51 +21,58 @@ let themeInitScript = '';
|
|
|
23
21
|
} catch (e) { /* ignore */ }
|
|
24
22
|
})();
|
|
25
23
|
|
|
26
|
-
//
|
|
24
|
+
// Basic whitespace cleanup (keep this simple version)
|
|
27
25
|
function cleanupHtml(html) {
|
|
28
26
|
if (!html) return '';
|
|
29
|
-
return html
|
|
30
|
-
.replace(/^[ \t]+$/gm, '')
|
|
31
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
32
|
-
.trim();
|
|
27
|
+
return html.replace(/^\s*[\r\n]/gm, '').trim();
|
|
33
28
|
}
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
function fixHtmlLinks(htmlContent, relativePathToRoot, isOfflineMode) {
|
|
30
|
+
function fixHtmlLinks(htmlContent, relativePathToRoot, isOfflineMode, configBase = '/') {
|
|
37
31
|
if (!htmlContent) return '';
|
|
38
32
|
const root = relativePathToRoot || './';
|
|
33
|
+
const baseUrl = configBase.endsWith('/') ? configBase : configBase + '/';
|
|
39
34
|
|
|
40
|
-
return htmlContent.replace(/href="(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Convert absolute project paths to relative
|
|
44
|
-
if (href.startsWith('/')) {
|
|
45
|
-
finalPath = root + href.substring(1);
|
|
35
|
+
return htmlContent.replace(/(href|src)=["']([^"']+)["']/g, (match, attr, url) => {
|
|
36
|
+
if (url.startsWith('#') || url.startsWith('http') || url.startsWith('mailto:') || url === '') {
|
|
37
|
+
return match;
|
|
46
38
|
}
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
|
|
40
|
+
let finalPath = url;
|
|
41
|
+
|
|
42
|
+
// 1. Handle Base URL removal
|
|
43
|
+
if (baseUrl !== '/' && url.startsWith(baseUrl)) {
|
|
44
|
+
finalPath = '/' + url.substring(baseUrl.length);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. Handle Absolute Paths
|
|
48
|
+
if (finalPath.startsWith('/')) {
|
|
49
|
+
// Simple logic: if root relative, prepend relative path
|
|
50
|
+
finalPath = root + finalPath.substring(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. Offline Mode Logic
|
|
49
54
|
if (isOfflineMode) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const [pathOnly] = finalPath.split(/[?#]/);
|
|
56
|
+
const ext = path.extname(pathOnly);
|
|
57
|
+
const isAsset = ['.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico'].includes(ext.toLowerCase());
|
|
58
|
+
|
|
59
|
+
if (!isAsset && !ext) {
|
|
60
|
+
if (finalPath.endsWith('/')) {
|
|
61
|
+
finalPath += 'index.html';
|
|
62
|
+
} else if (!finalPath.includes('#')) {
|
|
63
|
+
finalPath += '/index.html';
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
} else {
|
|
61
|
-
// Web mode (strip index.html for clean URLs)
|
|
62
67
|
if (finalPath.endsWith('/index.html')) {
|
|
63
68
|
finalPath = finalPath.substring(0, finalPath.length - 10);
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
|
-
|
|
71
|
+
|
|
72
|
+
return `${attr}="${finalPath}"`;
|
|
67
73
|
});
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
// aggregates HTML snippets from various plugins (SEO, Analytics, etc.)
|
|
71
76
|
async function processPluginHooks(config, pageData, relativePathToRoot) {
|
|
72
77
|
let metaTagsHtml = '';
|
|
73
78
|
let faviconLinkHtml = '';
|
|
@@ -77,17 +82,16 @@ async function processPluginHooks(config, pageData, relativePathToRoot) {
|
|
|
77
82
|
let pluginBodyScriptsHtml = '';
|
|
78
83
|
|
|
79
84
|
const safeRoot = relativePathToRoot || './';
|
|
80
|
-
const indent = ' '; // 4 spaces for cleaner output
|
|
81
85
|
|
|
82
86
|
if (config.favicon) {
|
|
83
87
|
const cleanFaviconPath = config.favicon.startsWith('/') ? config.favicon.substring(1) : config.favicon;
|
|
84
88
|
const finalFaviconHref = `${safeRoot}${cleanFaviconPath}`;
|
|
85
|
-
faviconLinkHtml =
|
|
89
|
+
faviconLinkHtml = `<link rel="icon" href="${finalFaviconHref}" type="image/x-icon" sizes="any">\n<link rel="shortcut icon" href="${finalFaviconHref}" type="image/x-icon">`;
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
if (config.theme && config.theme.name && config.theme.name !== 'default') {
|
|
89
93
|
const themeCssPath = `assets/css/docmd-theme-${config.theme.name}.css`;
|
|
90
|
-
themeCssLinkHtml =
|
|
94
|
+
themeCssLinkHtml = `<link rel="stylesheet" href="${safeRoot}${themeCssPath}">`;
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
if (config.plugins?.seo) {
|
|
@@ -103,7 +107,6 @@ async function processPluginHooks(config, pageData, relativePathToRoot) {
|
|
|
103
107
|
return { metaTagsHtml, faviconLinkHtml, themeCssLinkHtml, pluginStylesHtml, pluginHeadScriptsHtml, pluginBodyScriptsHtml };
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
// Main function to assemble the page data and render the EJS template
|
|
107
110
|
async function generateHtmlPage(templateData, isOfflineMode = false) {
|
|
108
111
|
let { content, frontmatter, outputPath, headings, config } = templateData;
|
|
109
112
|
const { currentPagePath, prevPage, nextPage, relativePathToRoot, navigationHtml, siteTitle } = templateData;
|
|
@@ -111,19 +114,16 @@ async function generateHtmlPage(templateData, isOfflineMode = false) {
|
|
|
111
114
|
|
|
112
115
|
if (!relativePathToRoot) templateData.relativePathToRoot = './';
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
content = fixHtmlLinks(content, templateData.relativePathToRoot, isOfflineMode);
|
|
117
|
+
content = fixHtmlLinks(content, templateData.relativePathToRoot, isOfflineMode, config.base);
|
|
116
118
|
const pluginOutputs = await processPluginHooks(config, { frontmatter, outputPath }, templateData.relativePathToRoot);
|
|
117
119
|
|
|
118
|
-
// Process footer markdown if present
|
|
119
120
|
let footerHtml = '';
|
|
120
121
|
if (config.footer) {
|
|
121
122
|
if (!mdInstance) mdInstance = createMarkdownItInstance(config);
|
|
122
123
|
footerHtml = mdInstance.renderInline(config.footer);
|
|
123
|
-
footerHtml = fixHtmlLinks(footerHtml, templateData.relativePathToRoot, isOfflineMode);
|
|
124
|
+
footerHtml = fixHtmlLinks(footerHtml, templateData.relativePathToRoot, isOfflineMode, config.base);
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
// Determine which template to use
|
|
127
127
|
let templateName = frontmatter.noStyle === true ? 'no-style.ejs' : 'layout.ejs';
|
|
128
128
|
const layoutTemplatePath = path.join(__dirname, '..', 'templates', templateName);
|
|
129
129
|
if (!await fs.exists(layoutTemplatePath)) throw new Error(`Template not found: ${layoutTemplatePath}`);
|
|
@@ -131,7 +131,6 @@ async function generateHtmlPage(templateData, isOfflineMode = false) {
|
|
|
131
131
|
|
|
132
132
|
const isActivePage = currentPagePath && content && content.trim().length > 0;
|
|
133
133
|
|
|
134
|
-
// Build the "Edit this page" link
|
|
135
134
|
let editUrl = null;
|
|
136
135
|
let editLinkText = 'Edit this page';
|
|
137
136
|
if (config.editLink && config.editLink.enabled && config.editLink.baseUrl) {
|
|
@@ -141,10 +140,9 @@ async function generateHtmlPage(templateData, isOfflineMode = false) {
|
|
|
141
140
|
editLinkText = config.editLink.text || editLinkText;
|
|
142
141
|
}
|
|
143
142
|
|
|
144
|
-
// Prepare complete data object for EJS
|
|
145
143
|
const ejsData = {
|
|
146
144
|
...templateData,
|
|
147
|
-
description: frontmatter.description || '',
|
|
145
|
+
description: frontmatter.description || '',
|
|
148
146
|
footerHtml, editUrl, editLinkText, isActivePage,
|
|
149
147
|
defaultMode: config.theme?.defaultMode || 'light',
|
|
150
148
|
logo: config.logo, sidebarConfig: config.sidebar || {}, theme: config.theme,
|
|
@@ -155,13 +153,12 @@ async function generateHtmlPage(templateData, isOfflineMode = false) {
|
|
|
155
153
|
isOfflineMode
|
|
156
154
|
};
|
|
157
155
|
|
|
158
|
-
// Render and format
|
|
159
156
|
const rawHtml = renderHtmlPage(layoutTemplate, ejsData, layoutTemplatePath);
|
|
160
157
|
const pkgVersion = require('../../package.json').version;
|
|
161
158
|
const brandingComment = `<!-- Generated by docmd (v${pkgVersion}) - https://docmd.io -->\n`;
|
|
162
159
|
|
|
163
|
-
//
|
|
164
|
-
return brandingComment +
|
|
160
|
+
// REMOVED: formatHtml(rawHtml)
|
|
161
|
+
return brandingComment + cleanupHtml(rawHtml);
|
|
165
162
|
}
|
|
166
163
|
|
|
167
164
|
function renderHtmlPage(templateContent, ejsData, filename = 'template.ejs', options = {}) {
|
|
@@ -173,14 +170,12 @@ function renderHtmlPage(templateContent, ejsData, filename = 'template.ejs', opt
|
|
|
173
170
|
}
|
|
174
171
|
}
|
|
175
172
|
|
|
176
|
-
// Generate the sidebar navigation HTML separately
|
|
177
173
|
async function generateNavigationHtml(navItems, currentPagePath, relativePathToRoot, config, isOfflineMode = false) {
|
|
178
174
|
const navTemplatePath = path.join(__dirname, '..', 'templates', 'navigation.ejs');
|
|
179
175
|
if (!await fs.exists(navTemplatePath)) throw new Error(`Navigation template not found: ${navTemplatePath}`);
|
|
180
176
|
const navTemplate = await fs.readFile(navTemplatePath, 'utf8');
|
|
181
177
|
const safeRoot = relativePathToRoot || './';
|
|
182
178
|
|
|
183
|
-
// We render raw here; the main page formatter will clean this up later
|
|
184
179
|
return ejs.render(navTemplate, {
|
|
185
180
|
navItems, currentPagePath, relativePathToRoot: safeRoot, config, isOfflineMode, renderIcon
|
|
186
181
|
}, { filename: navTemplatePath });
|
|
@@ -1,14 +1,49 @@
|
|
|
1
1
|
// Source file from the docmd project — https://github.com/docmd-io/docmd
|
|
2
2
|
|
|
3
|
-
const MarkdownIt = require('markdown-it');
|
|
3
|
+
const MarkdownIt = require('markdown-it');
|
|
4
4
|
const { containers } = require('./containers');
|
|
5
5
|
|
|
6
|
-
// --- Helper:
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
// --- Helper: Smart Dedent ---
|
|
7
|
+
// This handles the "Tab Indentation" and "Code Block" nesting issues
|
|
8
|
+
function smartDedent(str) {
|
|
9
|
+
const lines = str.split('\n');
|
|
10
|
+
|
|
11
|
+
// 1. Calculate global minimum indent (ignoring empty lines)
|
|
12
|
+
let minIndent = Infinity;
|
|
13
|
+
lines.forEach(line => {
|
|
14
|
+
if (line.trim().length === 0) return;
|
|
15
|
+
const match = line.match(/^ */);
|
|
16
|
+
const indent = match ? match[0].length : 0;
|
|
17
|
+
if (indent < minIndent) minIndent = indent;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (minIndent === Infinity) return str;
|
|
21
|
+
|
|
22
|
+
// 2. Strip the common indent
|
|
23
|
+
const dedented = lines.map(line => {
|
|
24
|
+
if (line.trim().length === 0) return '';
|
|
25
|
+
return line.substring(minIndent);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 3. Fix Code Fences
|
|
29
|
+
// If a line looks like a code fence (```) but is still indented 4+ spaces,
|
|
30
|
+
// it will be parsed as an "Indented Code Block" containing text, not a Fence.
|
|
31
|
+
// We force-pull these specific lines to the left to ensure they render as Fences.
|
|
32
|
+
return dedented.map(line => {
|
|
33
|
+
// Regex: 4 or more spaces, followed by 3 or more backticks/tildes
|
|
34
|
+
if (/^\s{4,}(`{3,}|~{3,})/.test(line)) {
|
|
35
|
+
return line.trimStart();
|
|
36
|
+
}
|
|
37
|
+
return line;
|
|
38
|
+
}).join('\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Helper to check if a line starts a fence
|
|
42
|
+
function isFenceLine(line) {
|
|
43
|
+
return /^(\s{0,3})(~{3,}|`{3,})/.test(line);
|
|
44
|
+
}
|
|
10
45
|
|
|
11
|
-
// --- 1. Advanced Container Rule
|
|
46
|
+
// --- 1. Advanced Container Rule ---
|
|
12
47
|
function advancedContainerRule(state, startLine, endLine, silent) {
|
|
13
48
|
const start = state.bMarks[startLine] + state.tShift[startLine];
|
|
14
49
|
const max = state.eMarks[startLine];
|
|
@@ -33,6 +68,7 @@ function advancedContainerRule(state, startLine, endLine, silent) {
|
|
|
33
68
|
let nextLine = startLine;
|
|
34
69
|
let found = false;
|
|
35
70
|
let depth = 1;
|
|
71
|
+
let inFence = false;
|
|
36
72
|
|
|
37
73
|
while (nextLine < endLine) {
|
|
38
74
|
nextLine++;
|
|
@@ -40,23 +76,28 @@ function advancedContainerRule(state, startLine, endLine, silent) {
|
|
|
40
76
|
const nextMax = state.eMarks[nextLine];
|
|
41
77
|
const nextContent = state.src.slice(nextStart, nextMax).trim();
|
|
42
78
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
79
|
+
// Check for fences to prevent parsing ::: inside code blocks (#42)
|
|
80
|
+
if (isFenceLine(nextContent)) inFence = !inFence;
|
|
81
|
+
|
|
82
|
+
if (!inFence) {
|
|
83
|
+
if (nextContent.startsWith(':::')) {
|
|
84
|
+
const containerMatch = nextContent.match(/^:::\s*(\w+)/);
|
|
85
|
+
if (containerMatch && containerMatch[1] !== containerName) {
|
|
86
|
+
const innerContainer = containers[containerMatch[1]];
|
|
87
|
+
if (innerContainer && innerContainer.render && !innerContainer.selfClosing) {
|
|
88
|
+
depth++;
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (nextContent === ':::') {
|
|
95
|
+
depth--;
|
|
96
|
+
if (depth === 0) {
|
|
97
|
+
found = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
49
100
|
}
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (nextContent === ':::') {
|
|
55
|
-
depth--;
|
|
56
|
-
if (depth === 0) {
|
|
57
|
-
found = true;
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
101
|
}
|
|
61
102
|
}
|
|
62
103
|
|
|
@@ -81,7 +122,7 @@ function advancedContainerRule(state, startLine, endLine, silent) {
|
|
|
81
122
|
return true;
|
|
82
123
|
}
|
|
83
124
|
|
|
84
|
-
// --- 2. Changelog Timeline Rule
|
|
125
|
+
// --- 2. Changelog Timeline Rule ---
|
|
85
126
|
function changelogTimelineRule(state, startLine, endLine, silent) {
|
|
86
127
|
const start = state.bMarks[startLine] + state.tShift[startLine];
|
|
87
128
|
const max = state.eMarks[startLine];
|
|
@@ -93,6 +134,7 @@ function changelogTimelineRule(state, startLine, endLine, silent) {
|
|
|
93
134
|
let nextLine = startLine;
|
|
94
135
|
let found = false;
|
|
95
136
|
let depth = 1;
|
|
137
|
+
let inFence = false;
|
|
96
138
|
|
|
97
139
|
while (nextLine < endLine) {
|
|
98
140
|
nextLine++;
|
|
@@ -100,21 +142,21 @@ function changelogTimelineRule(state, startLine, endLine, silent) {
|
|
|
100
142
|
const nextMax = state.eMarks[nextLine];
|
|
101
143
|
const nextContent = state.src.slice(nextStart, nextMax).trim();
|
|
102
144
|
|
|
103
|
-
if (nextContent
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
145
|
+
if (isFenceLine(nextContent)) inFence = !inFence;
|
|
146
|
+
|
|
147
|
+
if (!inFence) {
|
|
148
|
+
if (nextContent.startsWith(':::')) {
|
|
149
|
+
const match = nextContent.match(/^:::\s*(\w+)/);
|
|
150
|
+
if (match) {
|
|
151
|
+
const containerName = match[1];
|
|
152
|
+
const containerDef = containers[containerName];
|
|
153
|
+
if (!containerDef || !containerDef.selfClosing) depth++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (nextContent === ':::') {
|
|
157
|
+
depth--;
|
|
158
|
+
if (depth === 0) { found = true; break; }
|
|
111
159
|
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (nextContent === ':::') {
|
|
116
|
-
depth--;
|
|
117
|
-
if (depth === 0) { found = true; break; }
|
|
118
160
|
}
|
|
119
161
|
}
|
|
120
162
|
|
|
@@ -130,25 +172,26 @@ function changelogTimelineRule(state, startLine, endLine, silent) {
|
|
|
130
172
|
const lines = content.split('\n');
|
|
131
173
|
const entries = [];
|
|
132
174
|
let currentEntry = null;
|
|
133
|
-
let
|
|
175
|
+
let currentContentLines = [];
|
|
134
176
|
|
|
135
177
|
for (let i = 0; i < lines.length; i++) {
|
|
136
|
-
const
|
|
137
|
-
const
|
|
178
|
+
const rawLine = lines[i];
|
|
179
|
+
const trimmedLine = rawLine.trim();
|
|
180
|
+
const markerMatch = trimmedLine.match(/^==\s+(.+)$/);
|
|
138
181
|
|
|
139
182
|
if (markerMatch) {
|
|
140
183
|
if (currentEntry) {
|
|
141
|
-
currentEntry.content =
|
|
184
|
+
currentEntry.content = smartDedent(currentContentLines.join('\n'));
|
|
142
185
|
entries.push(currentEntry);
|
|
143
186
|
}
|
|
144
187
|
currentEntry = { meta: markerMatch[1], content: '' };
|
|
145
|
-
|
|
188
|
+
currentContentLines = [];
|
|
146
189
|
} else if (currentEntry) {
|
|
147
|
-
|
|
190
|
+
currentContentLines.push(rawLine);
|
|
148
191
|
}
|
|
149
192
|
}
|
|
150
193
|
if (currentEntry) {
|
|
151
|
-
currentEntry.content =
|
|
194
|
+
currentEntry.content = smartDedent(currentContentLines.join('\n'));
|
|
152
195
|
entries.push(currentEntry);
|
|
153
196
|
}
|
|
154
197
|
|
|
@@ -160,7 +203,6 @@ function changelogTimelineRule(state, startLine, endLine, silent) {
|
|
|
160
203
|
<div class="changelog-meta"><span class="changelog-date">${entry.meta}</span></div>
|
|
161
204
|
<div class="changelog-body">`;
|
|
162
205
|
|
|
163
|
-
// This ensures callouts/cards inside changelogs are parsed
|
|
164
206
|
entryOpen.content += state.md.render(entry.content, state.env);
|
|
165
207
|
|
|
166
208
|
const entryClose = state.push('html_block', '', 0);
|
|
@@ -183,6 +225,7 @@ function stepsContainerRule(state, startLine, endLine, silent) {
|
|
|
183
225
|
let nextLine = startLine;
|
|
184
226
|
let found = false;
|
|
185
227
|
let depth = 1;
|
|
228
|
+
let inFence = false;
|
|
186
229
|
|
|
187
230
|
while (nextLine < endLine) {
|
|
188
231
|
nextLine++;
|
|
@@ -190,23 +233,23 @@ function stepsContainerRule(state, startLine, endLine, silent) {
|
|
|
190
233
|
const nextMax = state.eMarks[nextLine];
|
|
191
234
|
const nextContent = state.src.slice(nextStart, nextMax).trim();
|
|
192
235
|
|
|
193
|
-
if (nextContent
|
|
194
|
-
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
236
|
+
if (isFenceLine(nextContent)) inFence = !inFence;
|
|
237
|
+
|
|
238
|
+
if (!inFence) {
|
|
239
|
+
if (nextContent.startsWith('== tab')) continue;
|
|
240
|
+
if (nextContent.startsWith(':::')) {
|
|
241
|
+
const containerMatch = nextContent.match(/^:::\s*(\w+)/);
|
|
242
|
+
if (containerMatch) {
|
|
243
|
+
const containerName = containerMatch[1];
|
|
244
|
+
const innerContainer = containers[containerName];
|
|
245
|
+
if (innerContainer && !innerContainer.selfClosing) depth++;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (nextContent === ':::') {
|
|
250
|
+
depth--;
|
|
251
|
+
if (depth === 0) { found = true; break; }
|
|
202
252
|
}
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (nextContent === ':::') {
|
|
208
|
-
depth--;
|
|
209
|
-
if (depth === 0) { found = true; break; }
|
|
210
253
|
}
|
|
211
254
|
}
|
|
212
255
|
|
|
@@ -227,7 +270,7 @@ function stepsContainerRule(state, startLine, endLine, silent) {
|
|
|
227
270
|
return true;
|
|
228
271
|
}
|
|
229
272
|
|
|
230
|
-
// --- 4. Enhanced Tabs Rule ---
|
|
273
|
+
// --- 4. Enhanced Tabs Rule (Fixed) ---
|
|
231
274
|
function enhancedTabsRule(state, startLine, endLine, silent) {
|
|
232
275
|
const start = state.bMarks[startLine] + state.tShift[startLine];
|
|
233
276
|
const max = state.eMarks[startLine];
|
|
@@ -239,61 +282,69 @@ function enhancedTabsRule(state, startLine, endLine, silent) {
|
|
|
239
282
|
let nextLine = startLine;
|
|
240
283
|
let found = false;
|
|
241
284
|
let depth = 1;
|
|
285
|
+
let inFence = false;
|
|
286
|
+
|
|
242
287
|
while (nextLine < endLine) {
|
|
243
288
|
nextLine++;
|
|
244
289
|
const nextStart = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
245
290
|
const nextMax = state.eMarks[nextLine];
|
|
246
291
|
const nextContent = state.src.slice(nextStart, nextMax).trim();
|
|
247
292
|
|
|
248
|
-
if (nextContent
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
293
|
+
if (isFenceLine(nextContent)) inFence = !inFence;
|
|
294
|
+
|
|
295
|
+
if (!inFence) {
|
|
296
|
+
if (nextContent.startsWith(':::')) {
|
|
297
|
+
const containerMatch = nextContent.match(/^:::\s*(\w+)/);
|
|
298
|
+
if (containerMatch && containerMatch[1] !== 'tabs') {
|
|
299
|
+
if (containerMatch[1] === 'steps') continue;
|
|
300
|
+
const innerContainer = containers[containerMatch[1]];
|
|
301
|
+
if (innerContainer && !innerContainer.selfClosing) depth++;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (nextContent === ':::') {
|
|
306
|
+
depth--;
|
|
307
|
+
if (depth === 0) { found = true; break; }
|
|
308
|
+
}
|
|
261
309
|
}
|
|
262
310
|
}
|
|
263
311
|
if (!found) return false;
|
|
264
312
|
|
|
265
313
|
let content = '';
|
|
314
|
+
// Capture content preserving newlines
|
|
266
315
|
for (let i = startLine + 1; i < nextLine; i++) {
|
|
267
316
|
const lineStart = state.bMarks[i] + state.tShift[i];
|
|
268
317
|
const lineEnd = state.eMarks[i];
|
|
318
|
+
// .slice() keeps the indentation of the line as it is in source
|
|
269
319
|
content += state.src.slice(lineStart, lineEnd) + '\n';
|
|
270
320
|
}
|
|
271
321
|
|
|
272
322
|
const lines = content.split('\n');
|
|
273
323
|
const tabs = [];
|
|
274
324
|
let currentTab = null;
|
|
275
|
-
let
|
|
325
|
+
let currentContentLines = [];
|
|
276
326
|
|
|
277
327
|
for (let i = 0; i < lines.length; i++) {
|
|
278
|
-
const
|
|
279
|
-
const
|
|
328
|
+
const rawLine = lines[i];
|
|
329
|
+
const trimmedLine = rawLine.trim();
|
|
330
|
+
|
|
331
|
+
const tabMatch = trimmedLine.match(/^==\s*tab\s+(?:"([^"]+)"|(\S+))$/);
|
|
280
332
|
|
|
281
333
|
if (tabMatch) {
|
|
282
334
|
if (currentTab) {
|
|
283
|
-
|
|
335
|
+
// Apply Smart Dedent before saving
|
|
336
|
+
currentTab.content = smartDedent(currentContentLines.join('\n'));
|
|
284
337
|
tabs.push(currentTab);
|
|
285
338
|
}
|
|
286
339
|
const title = tabMatch[1] || tabMatch[2];
|
|
287
340
|
currentTab = { title: title, content: '' };
|
|
288
|
-
|
|
341
|
+
currentContentLines = [];
|
|
289
342
|
} else if (currentTab) {
|
|
290
|
-
|
|
291
|
-
currentContent.push(lines[i]);
|
|
292
|
-
}
|
|
343
|
+
currentContentLines.push(rawLine);
|
|
293
344
|
}
|
|
294
345
|
}
|
|
295
346
|
if (currentTab) {
|
|
296
|
-
currentTab.content =
|
|
347
|
+
currentTab.content = smartDedent(currentContentLines.join('\n'));
|
|
297
348
|
tabs.push(currentTab);
|
|
298
349
|
}
|
|
299
350
|
|
|
@@ -315,10 +366,8 @@ function enhancedTabsRule(state, startLine, endLine, silent) {
|
|
|
315
366
|
const paneToken = state.push('tab_pane_open', 'div', 1);
|
|
316
367
|
paneToken.attrs = [['class', `docmd-tab-pane ${index === 0 ? 'active' : ''}`]];
|
|
317
368
|
|
|
318
|
-
if (tab.content
|
|
319
|
-
|
|
320
|
-
// Since we are inside rules.js, we rely on state.md being available or fallback to simple render
|
|
321
|
-
const renderedContent = state.md.render(tab.content.trim(), state.env);
|
|
369
|
+
if (tab.content) {
|
|
370
|
+
const renderedContent = state.md.render(tab.content, state.env);
|
|
322
371
|
const htmlToken = state.push('html_block', '', 0);
|
|
323
372
|
htmlToken.content = renderedContent;
|
|
324
373
|
}
|
|
@@ -331,7 +380,6 @@ function enhancedTabsRule(state, startLine, endLine, silent) {
|
|
|
331
380
|
return true;
|
|
332
381
|
}
|
|
333
382
|
|
|
334
|
-
// --- 5. Standalone Closing Rule ---
|
|
335
383
|
const standaloneClosingRule = (state, startLine, endLine, silent) => {
|
|
336
384
|
const start = state.bMarks[startLine] + state.tShift[startLine];
|
|
337
385
|
const max = state.eMarks[startLine];
|
|
@@ -20,33 +20,25 @@ const headingIdPlugin = (md) => {
|
|
|
20
20
|
|
|
21
21
|
md.renderer.rules.heading_open = function(tokens, idx, options, env, self) {
|
|
22
22
|
const token = tokens[idx];
|
|
23
|
-
|
|
24
|
-
// Check if an ID was already set by markdown-it-attrs (e.g. {#my-id})
|
|
25
23
|
const existingId = token.attrGet('id');
|
|
26
24
|
|
|
27
|
-
// If NO ID exists, generate one automatically from the text content
|
|
28
25
|
if (!existingId) {
|
|
29
26
|
const contentToken = tokens[idx + 1];
|
|
30
27
|
if (contentToken && contentToken.type === 'inline' && contentToken.content) {
|
|
31
28
|
const headingText = contentToken.content;
|
|
32
|
-
|
|
33
|
-
// Note: markdown-it-attrs strips the curly braces content from .content
|
|
34
|
-
// BEFORE this rule runs, so headingText should be clean.
|
|
35
|
-
|
|
36
29
|
const id = headingText
|
|
37
30
|
.toLowerCase()
|
|
38
|
-
.replace(/\s+/g, '-')
|
|
39
|
-
.replace(/[^\w\u4e00-\u9fa5-]+/g, '')
|
|
40
|
-
.replace(/--+/g, '-')
|
|
41
|
-
.replace(/^-+/, '')
|
|
42
|
-
.replace(/-+$/, '');
|
|
31
|
+
.replace(/\s+/g, '-')
|
|
32
|
+
.replace(/[^\w\u4e00-\u9fa5-]+/g, '')
|
|
33
|
+
.replace(/--+/g, '-')
|
|
34
|
+
.replace(/^-+/, '')
|
|
35
|
+
.replace(/-+$/, '');
|
|
43
36
|
|
|
44
37
|
if (id) {
|
|
45
38
|
token.attrSet('id', id);
|
|
46
39
|
}
|
|
47
40
|
}
|
|
48
41
|
}
|
|
49
|
-
|
|
50
42
|
return originalHeadingOpen(tokens, idx, options, env, self);
|
|
51
43
|
};
|
|
52
44
|
};
|
|
@@ -59,13 +51,15 @@ function createMarkdownItInstance(config) {
|
|
|
59
51
|
breaks: true,
|
|
60
52
|
};
|
|
61
53
|
|
|
54
|
+
// Removed newlines from template literals to prevent extra padding in <pre> blocks
|
|
62
55
|
const highlightFn = (str, lang) => {
|
|
63
56
|
if (lang === 'mermaid') {
|
|
64
57
|
return `<pre class="mermaid">${new MarkdownIt().utils.escapeHtml(str)}</pre>`;
|
|
65
58
|
}
|
|
66
59
|
if (lang && hljs.getLanguage(lang)) {
|
|
67
60
|
try {
|
|
68
|
-
|
|
61
|
+
const highlighted = hljs.highlight(str, { language: lang, ignoreIllegals: true }).value;
|
|
62
|
+
return `<pre class="hljs"><code>${highlighted}</code></pre>`;
|
|
69
63
|
} catch (e) { console.error(e); }
|
|
70
64
|
}
|
|
71
65
|
return `<pre class="hljs"><code>${new MarkdownIt().utils.escapeHtml(str)}</code></pre>`;
|
|
@@ -78,7 +72,6 @@ function createMarkdownItInstance(config) {
|
|
|
78
72
|
|
|
79
73
|
const md = new MarkdownIt(mdOptions);
|
|
80
74
|
|
|
81
|
-
// Plugins
|
|
82
75
|
md.use(attrs, { leftDelimiter: '{', rightDelimiter: '}' });
|
|
83
76
|
md.use(markdown_it_footnote);
|
|
84
77
|
md.use(markdown_it_task_lists);
|
|
@@ -86,28 +79,24 @@ function createMarkdownItInstance(config) {
|
|
|
86
79
|
md.use(markdown_it_deflist);
|
|
87
80
|
md.use(headingIdPlugin);
|
|
88
81
|
|
|
89
|
-
// Register Containers
|
|
90
82
|
Object.keys(containers).forEach(containerName => {
|
|
91
83
|
const container = containers[containerName];
|
|
92
84
|
md.renderer.rules[`container_${containerName}_open`] = container.render;
|
|
93
85
|
md.renderer.rules[`container_${containerName}_close`] = container.render;
|
|
94
86
|
});
|
|
95
87
|
|
|
96
|
-
// Register Rules (Order Matters)
|
|
97
88
|
md.block.ruler.before('fence', 'steps_container', rules.stepsContainerRule, { alt: ['paragraph', 'reference', 'blockquote', 'list'] });
|
|
98
89
|
md.block.ruler.before('fence', 'enhanced_tabs', rules.enhancedTabsRule, { alt: ['paragraph', 'reference', 'blockquote', 'list'] });
|
|
99
90
|
md.block.ruler.before('fence', 'changelog_timeline', rules.changelogTimelineRule, { alt: ['paragraph', 'reference', 'blockquote', 'list'] });
|
|
100
91
|
md.block.ruler.before('paragraph', 'advanced_container', rules.advancedContainerRule, { alt: ['paragraph', 'reference', 'blockquote', 'list'] });
|
|
101
92
|
md.block.ruler.before('paragraph', 'standalone_closing', rules.standaloneClosingRule, { alt: ['paragraph', 'reference', 'blockquote', 'list'] });
|
|
102
93
|
|
|
103
|
-
// Register Renderers
|
|
104
94
|
md.renderer.rules.ordered_list_open = renderers.customOrderedListOpenRenderer;
|
|
105
95
|
md.renderer.rules.list_item_open = renderers.customListItemOpenRenderer;
|
|
106
96
|
md.renderer.rules.image = renderers.customImageRenderer;
|
|
107
97
|
md.renderer.rules.table_open = renderers.tableOpenRenderer;
|
|
108
98
|
md.renderer.rules.table_close = renderers.tableCloseRenderer;
|
|
109
99
|
|
|
110
|
-
// Tabs Renderers
|
|
111
100
|
md.renderer.rules.tabs_open = renderers.tabsOpenRenderer;
|
|
112
101
|
md.renderer.rules.tabs_nav_open = renderers.tabsNavOpenRenderer;
|
|
113
102
|
md.renderer.rules.tabs_nav_close = renderers.tabsNavCloseRenderer;
|
|
@@ -1,62 +1,74 @@
|
|
|
1
1
|
// Source file from the docmd project — https://github.com/docmd-io/docmd
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* @param {string} currentPagePath - The normalized path of the current page.
|
|
7
|
-
* @returns {{prevPage: object|null, nextPage: object|null}}
|
|
4
|
+
* Normalizes paths to a "canonical" form for comparison.
|
|
5
|
+
* Handles: "./path", "/path", "path/index.html", "path.md"
|
|
8
6
|
*/
|
|
7
|
+
function getCanonicalPath(p) {
|
|
8
|
+
if (!p) return '';
|
|
9
|
+
if (p.startsWith('http')) return p; // Don't touch external URLs
|
|
10
|
+
|
|
11
|
+
// 1. Remove leading dot-slash or slash
|
|
12
|
+
let path = p.replace(/^(\.\/|\/)+/, '');
|
|
13
|
+
|
|
14
|
+
// 2. Remove query strings or hashes
|
|
15
|
+
path = path.split('?')[0].split('#')[0];
|
|
16
|
+
|
|
17
|
+
// 3. Remove file extensions (.html, .md)
|
|
18
|
+
path = path.replace(/(\.html|\.md)$/, '');
|
|
19
|
+
|
|
20
|
+
// 4. Handle index files (folder/index -> folder)
|
|
21
|
+
if (path.endsWith('/index')) {
|
|
22
|
+
path = path.slice(0, -6);
|
|
23
|
+
} else if (path === 'index') {
|
|
24
|
+
path = '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 5. Remove trailing slash
|
|
28
|
+
if (path.endsWith('/')) {
|
|
29
|
+
path = path.slice(0, -1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
|
|
9
35
|
function findPageNeighbors(navItems, currentPagePath) {
|
|
10
36
|
const flatNavigation = [];
|
|
37
|
+
const currentCanonical = getCanonicalPath(currentPagePath);
|
|
11
38
|
|
|
12
|
-
|
|
13
|
-
function extractNavigationItems(items) {
|
|
39
|
+
function recurse(items) {
|
|
14
40
|
if (!items || !Array.isArray(items)) return;
|
|
15
|
-
|
|
16
|
-
for (const item of items) {
|
|
17
|
-
if (item.external || !item.path || item.path === '#') {
|
|
18
|
-
// If it's a category with no path but has children, recurse into them
|
|
19
|
-
if (item.children && Array.isArray(item.children)) {
|
|
20
|
-
extractNavigationItems(item.children);
|
|
21
|
-
}
|
|
22
|
-
continue; // Skip external links and parent items without a direct path
|
|
23
|
-
}
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
if (!
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
normalizedItemPath += '/';
|
|
42
|
+
for (const item of items) {
|
|
43
|
+
// Logic: Only consider items that have a path and are NOT external
|
|
44
|
+
// Also ignore items that are just '#' (placeholder parents)
|
|
45
|
+
if (item.path && !item.external && !item.path.startsWith('http') && item.path !== '#') {
|
|
46
|
+
flatNavigation.push({
|
|
47
|
+
title: item.title,
|
|
48
|
+
path: item.path, // Keep original path for the HREF
|
|
49
|
+
canonical: getCanonicalPath(item.path) // Use canonical for comparison
|
|
50
|
+
});
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
path: normalizedItemPath,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
if (item.children && Array.isArray(item.children)) {
|
|
43
|
-
extractNavigationItems(item.children);
|
|
53
|
+
if (item.children) {
|
|
54
|
+
recurse(item.children);
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
|
|
59
|
+
recurse(navItems);
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
// Find index using canonical paths
|
|
62
|
+
const index = flatNavigation.findIndex(item => item.canonical === currentCanonical);
|
|
51
63
|
|
|
52
|
-
if (
|
|
64
|
+
if (index === -1) {
|
|
53
65
|
return { prevPage: null, nextPage: null };
|
|
54
66
|
}
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
return {
|
|
69
|
+
prevPage: index > 0 ? flatNavigation[index - 1] : null,
|
|
70
|
+
nextPage: index < flatNavigation.length - 1 ? flatNavigation[index + 1] : null
|
|
71
|
+
};
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
module.exports = { findPageNeighbors };
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
* Initialize the theme from localStorage
|
|
5
|
-
*/
|
|
1
|
+
/* Source file from the docmd project — https://github.com/docmd-io/docmd */
|
|
6
2
|
|
|
7
3
|
(function() {
|
|
8
4
|
try {
|
|
9
|
-
// Determine Theme
|
|
10
5
|
var localValue = localStorage.getItem('docmd-theme');
|
|
11
6
|
var configValue = window.DOCMD_DEFAULT_MODE || 'light';
|
|
12
7
|
var theme = localValue ? localValue : configValue;
|
|
13
8
|
|
|
14
|
-
// Set HTML Attribute
|
|
9
|
+
// Set HTML Attribute
|
|
15
10
|
document.documentElement.setAttribute('data-theme', theme);
|
|
16
11
|
|
|
17
|
-
//
|
|
12
|
+
// Resolve 'system' to actual mode for Highlight.js
|
|
13
|
+
var effectiveTheme = theme;
|
|
14
|
+
if (theme === 'system') {
|
|
15
|
+
effectiveTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Handle Highlight.js Theme
|
|
18
19
|
var highlightLink = document.getElementById('highlight-theme');
|
|
19
20
|
if (highlightLink) {
|
|
20
21
|
var baseHref = highlightLink.getAttribute('data-base-href');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
highlightLink.href = baseHref + 'docmd-highlight-' + theme + '.css';
|
|
22
|
+
if (baseHref) {
|
|
23
|
+
// Force load the resolved theme (light/dark)
|
|
24
|
+
highlightLink.href = baseHref + 'docmd-highlight-' + effectiveTheme + '.css';
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
} catch (e) {
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// Source file from the docmd project — https://github.com/docmd-io/docmd
|
|
2
|
-
|
|
3
|
-
// Tags that don't have a closing tag (void elements)
|
|
4
|
-
const VOID_TAGS = new Set([
|
|
5
|
-
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
6
|
-
'link', 'meta', 'param', 'source', 'track', 'wbr', '!doctype'
|
|
7
|
-
]);
|
|
8
|
-
|
|
9
|
-
// Tags where whitespace should be preserved exactly
|
|
10
|
-
const PRESERVE_TAGS = new Set(['pre', 'script', 'style', 'textarea']);
|
|
11
|
-
|
|
12
|
-
function formatHtml(html) {
|
|
13
|
-
if (!html) return '';
|
|
14
|
-
|
|
15
|
-
const lines = html.split(/\r?\n/);
|
|
16
|
-
let formatted = [];
|
|
17
|
-
let indentLevel = 0;
|
|
18
|
-
const indentSize = 4; // 4 spaces per level
|
|
19
|
-
|
|
20
|
-
// State tracking
|
|
21
|
-
let inPreserveBlock = false;
|
|
22
|
-
let preserveTagName = '';
|
|
23
|
-
|
|
24
|
-
for (let i = 0; i < lines.length; i++) {
|
|
25
|
-
let line = lines[i].trim(); // Start with a clean slate for this line
|
|
26
|
-
|
|
27
|
-
if (!line) continue; // Skip empty lines
|
|
28
|
-
|
|
29
|
-
// --- 1. Handle Preserve Blocks (pre/script/style) ---
|
|
30
|
-
if (inPreserveBlock) {
|
|
31
|
-
// Check if this line ends the block
|
|
32
|
-
if (line.toLowerCase().includes(`</${preserveTagName}>`)) {
|
|
33
|
-
inPreserveBlock = false;
|
|
34
|
-
} else {
|
|
35
|
-
// If inside preserve block, add raw line (no trim) from original
|
|
36
|
-
formatted.push(lines[i]);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Check if this line STARTS a preserve block
|
|
42
|
-
for (const tag of PRESERVE_TAGS) {
|
|
43
|
-
if (line.toLowerCase().startsWith(`<${tag}`)) {
|
|
44
|
-
inPreserveBlock = true;
|
|
45
|
-
preserveTagName = tag;
|
|
46
|
-
// If it's a one-liner (e.g. <script>...</script>), handle it immediately
|
|
47
|
-
if (line.toLowerCase().includes(`</${tag}>`)) {
|
|
48
|
-
inPreserveBlock = false;
|
|
49
|
-
}
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// --- 2. Calculate Indentation ---
|
|
55
|
-
|
|
56
|
-
// Detect closing tags at the start of the line (e.g. </div> or -->)
|
|
57
|
-
const isClosing = /^<\//.test(line) || /^-->/.test(line);
|
|
58
|
-
|
|
59
|
-
if (isClosing && indentLevel > 0) {
|
|
60
|
-
indentLevel--;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Apply indentation
|
|
64
|
-
const spaces = ' '.repeat(indentLevel * indentSize);
|
|
65
|
-
formatted.push(spaces + line);
|
|
66
|
-
|
|
67
|
-
// --- 3. Adjust Indent for Next Line ---
|
|
68
|
-
|
|
69
|
-
// Detect opening tags
|
|
70
|
-
// Logic: Starts with <, not </, not <!, not void tag, and not self-closing />
|
|
71
|
-
const isOpening = /^<[\w]+/.test(line)
|
|
72
|
-
&& !/^<\//.test(line)
|
|
73
|
-
&& !/\/>$/.test(line); // Ends with />
|
|
74
|
-
|
|
75
|
-
if (isOpening) {
|
|
76
|
-
// Extract tag name to check if it's void
|
|
77
|
-
const tagNameMatch = line.match(/^<([\w-]+)/);
|
|
78
|
-
if (tagNameMatch) {
|
|
79
|
-
const tagName = tagNameMatch[1].toLowerCase();
|
|
80
|
-
if (!VOID_TAGS.has(tagName)) {
|
|
81
|
-
// Check if it's a one-liner: <div>Content</div>
|
|
82
|
-
// Logic: count opening vs closing tags in this line
|
|
83
|
-
// Simple heuristic: if line contains </tagName>, it's closed
|
|
84
|
-
const hasClosingPair = new RegExp(`</${tagName}>`, 'i').test(line);
|
|
85
|
-
|
|
86
|
-
if (!hasClosingPair) {
|
|
87
|
-
indentLevel++;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return formatted.join('\n');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
module.exports = { formatHtml };
|