rp-markdown-docs
Version:
A modern, beautiful documentation generator that converts markdown files into interactive HTML documentation sites
177 lines • 6.82 kB
JavaScript
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
import { glob } from 'glob';
import matter from 'gray-matter';
import MarkdownIt from 'markdown-it';
import anchor from 'markdown-it-anchor';
import toc from 'markdown-it-table-of-contents';
import Prism from 'prismjs';
import 'prismjs/components/prism-typescript.js';
import 'prismjs/components/prism-javascript.js';
import 'prismjs/components/prism-json.js';
import 'prismjs/components/prism-bash.js';
import 'prismjs/components/prism-css.js';
import 'prismjs/components/prism-markup.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export class DocumentationGenerator {
constructor(config) {
this.config = {
baseUrl: '/',
title: 'Documentation',
description: 'Technical Documentation',
theme: {
name: 'dark-fantasy',
primaryColor: '#8B5CF6',
accentColor: '#A855F7',
backgroundColor: '#0F0F23',
surfaceColor: '#1E1E3F',
textColor: '#E0E7FF',
borderColor: '#3730A3'
},
features: {
search: true,
tableOfContents: true,
darkMode: true
},
...config
};
this.md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
highlight: (str, lang) => {
if (lang && Prism.languages[lang]) {
try {
return `<pre class="language-${lang}"><code class="language-${lang}">${Prism.highlight(str, Prism.languages[lang], lang)}</code></pre>`;
}
catch (__) { }
}
return `<pre><code>${this.md.utils.escapeHtml(str)}</code></pre>`;
}
})
.use(anchor, {
permalink: anchor.permalink.headerLink()
})
.use(toc, {
includeLevel: [2, 3, 4]
});
}
async generate() {
// Ensure output directory exists
await fs.ensureDir(this.config.outputDir);
// Parse markdown files
const pages = await this.parseMarkdownFiles();
// Generate navigation structure
const navigation = this.buildNavigation(pages);
// Copy static assets
await this.copyStaticAssets();
// Generate HTML files
await this.generateHTMLFiles(pages, navigation);
// Generate search index
if (this.config.features?.search) {
await this.generateSearchIndex(pages);
}
}
async parseMarkdownFiles() {
const pattern = path.join(this.config.inputDir, '**/*.md');
const files = await glob(pattern);
const pages = [];
for (const file of files) {
const content = await fs.readFile(file, 'utf-8');
const { data: frontmatter, content: markdown } = matter(content);
const relativePath = path.relative(this.config.inputDir, file);
const id = relativePath.replace(/\.md$/, '').replace(/\\/g, '/');
const urlPath = id === 'index' ? '/' : `/${id}/`;
pages.push({
id,
title: frontmatter.title || this.extractTitle(markdown) || path.basename(file, '.md'),
content: markdown,
path: urlPath,
metadata: {
...frontmatter,
lastModified: (await fs.stat(file)).mtime.toISOString(),
filePath: relativePath
}
});
}
return pages.sort((a, b) => a.path.localeCompare(b.path));
}
extractTitle(markdown) {
const match = markdown.match(/^#\s+(.+)$/m);
return match ? match[1].trim() : null;
}
buildNavigation(pages) {
const nav = {};
for (const page of pages) {
const parts = page.id.split('/');
let current = nav;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (!current[part]) {
current[part] = {
title: part,
children: {},
page: null
};
}
if (i === parts.length - 1) {
current[part].page = page;
current[part].title = page.title;
}
current = current[part].children;
}
}
return this.convertNavToArray(nav);
}
convertNavToArray(nav) {
return Object.keys(nav).map(key => {
const item = nav[key];
return {
id: key,
title: item.title,
path: item.page?.path,
children: Object.keys(item.children).length > 0 ? this.convertNavToArray(item.children) : undefined,
metadata: item.page?.metadata
};
});
}
async copyStaticAssets() {
const templateDir = path.join(__dirname, '../../templates');
// Copy CSS and JS assets
await fs.copy(path.join(templateDir, 'assets'), path.join(this.config.outputDir, 'assets'), { overwrite: true });
}
async generateHTMLFiles(pages, navigation) {
const templatePath = path.join(__dirname, '../../templates/index.html');
const template = await fs.readFile(templatePath, 'utf-8');
// Generate main index.html with SPA
const indexHtml = template
.replace('{{TITLE}}', this.config.title || 'Documentation')
.replace('{{DESCRIPTION}}', this.config.description || 'Technical Documentation')
.replace('{{BASE_URL}}', this.config.baseUrl || '/')
.replace('{{CONFIG}}', JSON.stringify({
...this.config,
pages: pages.map(p => ({
id: p.id,
title: p.title,
path: p.path,
content: this.md.render(p.content),
metadata: p.metadata
})),
navigation
}));
await fs.writeFile(path.join(this.config.outputDir, 'index.html'), indexHtml);
}
async generateSearchIndex(pages) {
const searchIndex = pages.map(page => ({
id: page.id,
title: page.title,
content: page.content.replace(/[#*`]/g, '').substring(0, 500),
path: page.path,
tags: page.metadata.tags || []
}));
await fs.writeFile(path.join(this.config.outputDir, 'search-index.json'), JSON.stringify(searchIndex, null, 2));
}
}
//# sourceMappingURL=DocumentationGenerator.js.map