UNPKG

rp-markdown-docs

Version:

A modern, beautiful documentation generator that converts markdown files into interactive HTML documentation sites

177 lines 6.82 kB
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