Skip to content

Commit 32da491

Browse files
committed
Refactor HTML generation and markdown rules
Set default config.base to '/'. Remove the legacy html-formatter and simplify HTML cleanup; enhance link rewriting to handle href/src attributes, base URLs, offline mode, and asset detection. Update html-generator to use the new cleanup logic, remove smart formatter usage, and streamline plugin output for favicons and theme CSS. Revamp markdown rules with smartDedent, fence-aware parsing and improved nesting handling for containers, tabs, steps and changelogs, and fix tab content rendering. Tweak markdown setup for heading ID generation and safer code highlighting output. Improve navigation helper by normalizing canonical paths (strip extensions, index handling, queries/hashes) and using canonical comparison to find previous/next pages. (Deletes src/core/html-formatter.js and updates several core files.)
1 parent 134bf73 commit 32da491

File tree

6 files changed

+237
-289
lines changed

6 files changed

+237
-289
lines changed

src/core/config-loader.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ async function loadConfig(configPath) {
3838
validateConfig(config);
3939

4040
// Basic validation and defaults
41+
config.base = config.base || '/';
4142
config.srcDir = config.srcDir || 'docs';
4243
config.outputDir = config.outputDir || 'site';
4344
config.theme = config.theme || {};

src/core/html-formatter.js

Lines changed: 0 additions & 97 deletions
This file was deleted.

src/core/html-generator.js

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ const { createMarkdownItInstance } = require('./file-processor');
77
const { generateSeoMetaTags } = require('../plugins/seo');
88
const { generateAnalyticsScripts } = require('../plugins/analytics');
99
const { renderIcon } = require('./icon-renderer');
10-
const { formatHtml } = require('./html-formatter');
1110

1211
let mdInstance = null;
1312
let themeInitScript = '';
1413

15-
// Load the theme initialization script into memory once to avoid repeated disk I/O
1614
(async () => {
1715
try {
1816
const themeInitPath = path.join(__dirname, '..', 'templates', 'partials', 'theme-init.js');
@@ -23,51 +21,58 @@ let themeInitScript = '';
2321
} catch (e) { /* ignore */ }
2422
})();
2523

26-
// Removes excessive whitespace and blank lines from the generated HTML
24+
// Basic whitespace cleanup (keep this simple version)
2725
function cleanupHtml(html) {
2826
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();
3328
}
3429

35-
// Rewrites links based on build mode (Offline/File protocol vs Web Server)
36-
function fixHtmlLinks(htmlContent, relativePathToRoot, isOfflineMode) {
30+
function fixHtmlLinks(htmlContent, relativePathToRoot, isOfflineMode, configBase = '/') {
3731
if (!htmlContent) return '';
3832
const root = relativePathToRoot || './';
33+
const baseUrl = configBase.endsWith('/') ? configBase : configBase + '/';
3934

40-
return htmlContent.replace(/href="((?:\/|\.\/|\.\.\/)[^"]*)"/g, (match, href) => {
41-
let finalPath = href;
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;
4638
}
47-
48-
// Handle offline mode (force index.html for directories)
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
4954
if (isOfflineMode) {
50-
const cleanPath = finalPath.split('#')[0].split('?')[0];
51-
if (!path.extname(cleanPath)) {
52-
if (finalPath.includes('#')) {
53-
const parts = finalPath.split('#');
54-
const prefix = parts[0].endsWith('/') ? parts[0] : parts[0] + '/';
55-
finalPath = prefix + 'index.html#' + parts[1];
56-
} else {
57-
finalPath += (finalPath.endsWith('/') ? '' : '/') + 'index.html';
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';
5864
}
5965
}
6066
} else {
61-
// Web mode (strip index.html for clean URLs)
6267
if (finalPath.endsWith('/index.html')) {
6368
finalPath = finalPath.substring(0, finalPath.length - 10);
6469
}
6570
}
66-
return `href="${finalPath}"`;
71+
72+
return `${attr}="${finalPath}"`;
6773
});
6874
}
6975

70-
// aggregates HTML snippets from various plugins (SEO, Analytics, etc.)
7176
async function processPluginHooks(config, pageData, relativePathToRoot) {
7277
let metaTagsHtml = '';
7378
let faviconLinkHtml = '';
@@ -77,17 +82,16 @@ async function processPluginHooks(config, pageData, relativePathToRoot) {
7782
let pluginBodyScriptsHtml = '';
7883

7984
const safeRoot = relativePathToRoot || './';
80-
const indent = ' '; // 4 spaces for cleaner output
8185

8286
if (config.favicon) {
8387
const cleanFaviconPath = config.favicon.startsWith('/') ? config.favicon.substring(1) : config.favicon;
8488
const finalFaviconHref = `${safeRoot}${cleanFaviconPath}`;
85-
faviconLinkHtml = `${indent}<link rel="icon" href="${finalFaviconHref}" type="image/x-icon" sizes="any">\n${indent}<link rel="shortcut icon" href="${finalFaviconHref}" type="image/x-icon">`;
89+
faviconLinkHtml = `<link rel="icon" href="${finalFaviconHref}" type="image/x-icon" sizes="any">\n<link rel="shortcut icon" href="${finalFaviconHref}" type="image/x-icon">`;
8690
}
8791

8892
if (config.theme && config.theme.name && config.theme.name !== 'default') {
8993
const themeCssPath = `assets/css/docmd-theme-${config.theme.name}.css`;
90-
themeCssLinkHtml = `${indent}<link rel="stylesheet" href="${safeRoot}${themeCssPath}">`;
94+
themeCssLinkHtml = `<link rel="stylesheet" href="${safeRoot}${themeCssPath}">`;
9195
}
9296

9397
if (config.plugins?.seo) {
@@ -103,35 +107,30 @@ async function processPluginHooks(config, pageData, relativePathToRoot) {
103107
return { metaTagsHtml, faviconLinkHtml, themeCssLinkHtml, pluginStylesHtml, pluginHeadScriptsHtml, pluginBodyScriptsHtml };
104108
}
105109

106-
// Main function to assemble the page data and render the EJS template
107110
async function generateHtmlPage(templateData, isOfflineMode = false) {
108111
let { content, frontmatter, outputPath, headings, config } = templateData;
109112
const { currentPagePath, prevPage, nextPage, relativePathToRoot, navigationHtml, siteTitle } = templateData;
110113
const pageTitle = frontmatter.title;
111114

112115
if (!relativePathToRoot) templateData.relativePathToRoot = './';
113116

114-
// Process content links and generate plugin assets
115-
content = fixHtmlLinks(content, templateData.relativePathToRoot, isOfflineMode);
117+
content = fixHtmlLinks(content, templateData.relativePathToRoot, isOfflineMode, config.base);
116118
const pluginOutputs = await processPluginHooks(config, { frontmatter, outputPath }, templateData.relativePathToRoot);
117119

118-
// Process footer markdown if present
119120
let footerHtml = '';
120121
if (config.footer) {
121122
if (!mdInstance) mdInstance = createMarkdownItInstance(config);
122123
footerHtml = mdInstance.renderInline(config.footer);
123-
footerHtml = fixHtmlLinks(footerHtml, templateData.relativePathToRoot, isOfflineMode);
124+
footerHtml = fixHtmlLinks(footerHtml, templateData.relativePathToRoot, isOfflineMode, config.base);
124125
}
125126

126-
// Determine which template to use
127127
let templateName = frontmatter.noStyle === true ? 'no-style.ejs' : 'layout.ejs';
128128
const layoutTemplatePath = path.join(__dirname, '..', 'templates', templateName);
129129
if (!await fs.exists(layoutTemplatePath)) throw new Error(`Template not found: ${layoutTemplatePath}`);
130130
const layoutTemplate = await fs.readFile(layoutTemplatePath, 'utf8');
131131

132132
const isActivePage = currentPagePath && content && content.trim().length > 0;
133133

134-
// Build the "Edit this page" link
135134
let editUrl = null;
136135
let editLinkText = 'Edit this page';
137136
if (config.editLink && config.editLink.enabled && config.editLink.baseUrl) {
@@ -141,10 +140,9 @@ async function generateHtmlPage(templateData, isOfflineMode = false) {
141140
editLinkText = config.editLink.text || editLinkText;
142141
}
143142

144-
// Prepare complete data object for EJS
145143
const ejsData = {
146144
...templateData,
147-
description: frontmatter.description || '', // Fix for reference error
145+
description: frontmatter.description || '',
148146
footerHtml, editUrl, editLinkText, isActivePage,
149147
defaultMode: config.theme?.defaultMode || 'light',
150148
logo: config.logo, sidebarConfig: config.sidebar || {}, theme: config.theme,
@@ -155,13 +153,12 @@ async function generateHtmlPage(templateData, isOfflineMode = false) {
155153
isOfflineMode
156154
};
157155

158-
// Render and format
159156
const rawHtml = renderHtmlPage(layoutTemplate, ejsData, layoutTemplatePath);
160157
const pkgVersion = require('../../package.json').version;
161158
const brandingComment = `<!-- Generated by docmd (v${pkgVersion}) - https://docmd.io -->\n`;
162159

163-
// Apply smart formatting to the final HTML string
164-
return brandingComment + formatHtml(rawHtml);
160+
// REMOVED: formatHtml(rawHtml)
161+
return brandingComment + cleanupHtml(rawHtml);
165162
}
166163

167164
function renderHtmlPage(templateContent, ejsData, filename = 'template.ejs', options = {}) {
@@ -173,14 +170,12 @@ function renderHtmlPage(templateContent, ejsData, filename = 'template.ejs', opt
173170
}
174171
}
175172

176-
// Generate the sidebar navigation HTML separately
177173
async function generateNavigationHtml(navItems, currentPagePath, relativePathToRoot, config, isOfflineMode = false) {
178174
const navTemplatePath = path.join(__dirname, '..', 'templates', 'navigation.ejs');
179175
if (!await fs.exists(navTemplatePath)) throw new Error(`Navigation template not found: ${navTemplatePath}`);
180176
const navTemplate = await fs.readFile(navTemplatePath, 'utf8');
181177
const safeRoot = relativePathToRoot || './';
182178

183-
// We render raw here; the main page formatter will clean this up later
184179
return ejs.render(navTemplate, {
185180
navItems, currentPagePath, relativePathToRoot: safeRoot, config, isOfflineMode, renderIcon
186181
}, { filename: navTemplatePath });

0 commit comments

Comments
 (0)