UNPKG

svelte-markdown-pages

Version:

Build and render markdown-based content with distributed navigation for Svelte projects

531 lines (525 loc) 17 kB
'use strict'; var fs = require('fs'); var path = require('path'); var zod = require('zod'); var marked = require('marked'); // src/builder/static-generator.ts var DocItemTypeSchema = zod.z.enum(["section", "page"]); var DocItemSchema = zod.z.object({ name: zod.z.string().min(1), type: DocItemTypeSchema, label: zod.z.string().min(1), collapsed: zod.z.boolean().optional(), url: zod.z.string().url().optional() }); var IndexSchema = zod.z.object({ items: zod.z.array(DocItemSchema) }); // src/builder/parser.ts var ParserError = class extends Error { constructor(message, filePath) { super(message); this.filePath = filePath; this.name = "ParserError"; } }; function parseIndexFile(filePath) { try { const content = fs.readFileSync(filePath, "utf-8"); const data = JSON.parse(content); const result = IndexSchema.safeParse(data); if (!result.success) { throw new ParserError( `Invalid .index.json format: ${result.error.message}`, filePath ); } return result.data.items; } catch (error) { if (error instanceof ParserError) { throw error; } if (error instanceof SyntaxError) { throw new ParserError( `Invalid JSON in .index.json: ${error.message}`, filePath ); } throw new ParserError( `Failed to read .index.json: ${error instanceof Error ? error.message : "Unknown error"}`, filePath ); } } function autoDiscoverContent(dirPath) { try { const items = fs.readdirSync(dirPath, { withFileTypes: true }); const discoveredItems = []; const markdownFiles = items.filter((item) => item.isFile() && item.name.endsWith(".md")).map((item) => ({ ...item, name: item.name.slice(0, -3) })).sort((a, b) => a.name.localeCompare(b.name)); const directories = items.filter((item) => item.isDirectory() && !item.name.startsWith(".")).sort((a, b) => a.name.localeCompare(b.name)); for (const file of markdownFiles) { if (file.name.toLowerCase() !== "index" && file.name.toLowerCase() !== "readme") { discoveredItems.push({ name: file.name, type: "page", label: generateLabel(file.name) }); } } for (const dir of directories) { discoveredItems.push({ name: dir.name, type: "section", label: generateLabel(dir.name) }); } return discoveredItems; } catch (error) { throw new ParserError( `Failed to auto-discover content in directory: ${error instanceof Error ? error.message : "Unknown error"}`, dirPath ); } } function generateLabel(name) { return name.replace(/[-_]/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()).trim(); } function hasIndexFile(dirPath) { try { const indexPath = path.join(dirPath, ".index.json"); return fs.statSync(indexPath).isFile(); } catch { return false; } } function buildNavigationTree(contentPath, options = {}) { const { basePath = contentPath, validateFiles = true, autoDiscover = true } = options; try { if (!fs.statSync(contentPath).isDirectory()) { throw new ParserError(`Content path is not a directory: ${contentPath}`); } } catch (error) { if (error instanceof ParserError) { throw error; } throw new ParserError(`Content path does not exist: ${contentPath}`, contentPath); } const rootIndexPath = path.join(contentPath, ".index.json"); let rootItems; const hasIndex = hasIndexFile(contentPath); if (hasIndex) { try { rootItems = parseIndexFile(rootIndexPath); } catch (error) { if (error instanceof ParserError) { throw error; } throw new ParserError(`Root .index.json not found: ${rootIndexPath}`, rootIndexPath); } } else if (autoDiscover) { rootItems = autoDiscoverContent(contentPath); } else { throw new ParserError(`Root .index.json not found: ${rootIndexPath}`, rootIndexPath); } const navigationItems = []; for (const item of rootItems) { const navigationItem = processNavigationItem( item, contentPath, basePath, validateFiles, autoDiscover ); navigationItems.push(navigationItem); } return { items: navigationItems }; } function processNavigationItem(item, currentPath, basePath, validateFiles, autoDiscover) { const navigationItem = { ...item }; if (item.type === "section") { const sectionPath = path.join(currentPath, item.name); if (hasIndexFile(sectionPath)) { const sectionIndexPath = path.join(sectionPath, ".index.json"); if (validateFiles) { try { if (!fs.statSync(sectionIndexPath).isFile()) { throw new ParserError( `Section .index.json not found: ${sectionIndexPath}`, sectionIndexPath ); } } catch (error) { if (error instanceof ParserError) { throw error; } throw new ParserError( `Section .index.json not found: ${sectionIndexPath}`, sectionIndexPath ); } } try { const sectionItems = parseIndexFile(sectionIndexPath); navigationItem.items = sectionItems.map( (subItem) => processNavigationItem(subItem, sectionPath, basePath, validateFiles, autoDiscover) ); } catch (error) { if (error instanceof ParserError) { throw error; } throw new ParserError( `Failed to process section ${item.name}: ${error instanceof Error ? error.message : "Unknown error"}`, sectionIndexPath ); } } else if (autoDiscover) { try { const sectionItems = autoDiscoverContent(sectionPath); navigationItem.items = sectionItems.map( (subItem) => processNavigationItem(subItem, sectionPath, basePath, validateFiles, autoDiscover) ); } catch (error) { if (error instanceof ParserError) { throw error; } throw new ParserError( `Failed to auto-discover section ${item.name}: ${error instanceof Error ? error.message : "Unknown error"}`, sectionPath ); } } else { const sectionIndexPath = path.join(sectionPath, ".index.json"); throw new ParserError( `Section .index.json not found: ${sectionIndexPath}`, sectionIndexPath ); } const indexPath = path.join(sectionPath, "index.md"); const readmePath = path.join(sectionPath, "README.md"); const readmeLowerPath = path.join(sectionPath, "readme.md"); let sectionRootPath; if (validateFiles) { try { if (fs.statSync(indexPath).isFile()) { sectionRootPath = indexPath; } } catch { try { if (fs.statSync(readmePath).isFile()) { sectionRootPath = readmePath; } } catch { try { if (fs.statSync(readmeLowerPath).isFile()) { sectionRootPath = readmeLowerPath; } } catch { } } } } else { if (fs.existsSync(indexPath)) { sectionRootPath = indexPath; } else if (fs.existsSync(readmePath)) { sectionRootPath = readmePath; } else if (fs.existsSync(readmeLowerPath)) { sectionRootPath = readmeLowerPath; } } if (sectionRootPath) { navigationItem.path = path.relative(basePath, sectionRootPath); } } else if (item.type === "page") { const pagePath = path.join(currentPath, `${item.name}.md`); const relativePath = path.relative(basePath, pagePath); if (validateFiles) { try { if (!fs.statSync(pagePath).isFile()) { throw new ParserError( `Page markdown file not found: ${pagePath}`, pagePath ); } } catch (error) { if (error instanceof ParserError) { throw error; } throw new ParserError( `Page markdown file not found: ${pagePath}`, pagePath ); } } navigationItem.path = relativePath; } return navigationItem; } function validateContentStructure(contentPath, options = {}) { const { autoDiscover = true } = options; const errors = []; function validateDirectory(dirPath, depth = 0) { if (depth > 10) { errors.push(`Maximum directory depth exceeded: ${dirPath}`); return; } const indexPath = path.join(dirPath, ".index.json"); if (hasIndexFile(dirPath)) { try { const items = parseIndexFile(indexPath); for (const item of items) { if (item.type === "section") { const sectionPath = path.join(dirPath, item.name); try { if (!fs.statSync(sectionPath).isDirectory()) { errors.push(`Section directory not found: ${sectionPath}`); continue; } } catch (error) { errors.push(`Section directory not found: ${sectionPath}`); continue; } validateDirectory(sectionPath, depth + 1); } else if (item.type === "page") { const pagePath = path.join(dirPath, `${item.name}.md`); try { if (!fs.statSync(pagePath).isFile()) { errors.push(`Page markdown file not found: ${pagePath}`); } } catch (error) { errors.push(`Page markdown file not found: ${pagePath}`); } } } } catch (error) { errors.push(`Failed to parse ${indexPath}: ${error instanceof Error ? error.message : "Unknown error"}`); } } else if (autoDiscover) { try { const items = autoDiscoverContent(dirPath); for (const item of items) { if (item.type === "section") { const sectionPath = path.join(dirPath, item.name); try { if (!fs.statSync(sectionPath).isDirectory()) { errors.push(`Section directory not found: ${sectionPath}`); continue; } } catch (error) { errors.push(`Section directory not found: ${sectionPath}`); continue; } validateDirectory(sectionPath, depth + 1); } else if (item.type === "page") { const pagePath = path.join(dirPath, `${item.name}.md`); try { if (!fs.statSync(pagePath).isFile()) { errors.push(`Page markdown file not found: ${pagePath}`); } } catch (error) { errors.push(`Page markdown file not found: ${pagePath}`); } } } } catch (error) { errors.push(`Failed to auto-discover content in ${dirPath}: ${error instanceof Error ? error.message : "Unknown error"}`); } } else { errors.push(`Missing .index.json: ${indexPath}`); return; } } validateDirectory(contentPath); if (errors.length > 0) { throw new ParserError( `Content structure validation failed: ${errors.join("\n")}`, contentPath ); } } var BuilderError = class extends Error { constructor(message, filePath) { super(message); this.filePath = filePath; this.name = "BuilderError"; } }; function processMarkdown(content, processor) { if (processor) { content = processor.process(content); } return marked.marked(content); } function generateStaticPages(navigation, basePath, options = {}) { const pages = []; function processItems(items) { for (const item of items) { if (item.type === "page" && item.path) { const filePath = path.join(basePath, item.path); try { const markdownContent = fs.readFileSync(filePath, "utf-8"); const htmlContent = processMarkdown(markdownContent, options.processor); const fullHtml = generateHTMLPage(htmlContent, item.label, options.pageOptions); pages.push({ path: item.path.replace(/\.md$/, ".html"), content: markdownContent, html: fullHtml }); } catch (error) { throw new BuilderError( `Failed to process page ${item.path}: ${error instanceof Error ? error.message : "Unknown error"}`, filePath ); } } else if (item.items) { processItems(item.items); } } } processItems(navigation.items); return pages; } function generateHTMLPage(content, title, options = {}) { const pageTitle = options.title || title; const baseUrl = options.baseUrl || ""; const css = options.css || ""; const js = options.js || ""; return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${pageTitle}</title> <base href="${baseUrl}"> ${css ? `<style>${css}</style>` : ""} </head> <body> <div class="content"> ${content} </div> ${js ? `<script>${js}</script>` : ""} </body> </html>`; } // src/builder/static-generator.ts var StaticGeneratorError = class extends Error { constructor(message, filePath) { super(message); this.filePath = filePath; this.name = "StaticGeneratorError"; } }; async function generateStaticSite(contentPath, outputPath, options = {}) { try { validateContentStructure(contentPath, { autoDiscover: options.autoDiscover }); const navigation = buildNavigationTree(contentPath, { autoDiscover: options.autoDiscover }); const pages = generateStaticPages(navigation, contentPath, { processor: options.processor, pageOptions: { title: options.title, baseUrl: options.baseUrl, css: options.css, js: options.js } }); fs.mkdirSync(outputPath, { recursive: true }); for (const page of pages) { const pageOutputPath = path.join(outputPath, page.path); fs.mkdirSync(path.dirname(pageOutputPath), { recursive: true }); fs.writeFileSync(pageOutputPath, page.html); } let index; if (options.includeIndex !== false) { const indexPage = generateIndexPage(navigation, options); const indexPath = path.join(outputPath, "index.html"); fs.writeFileSync(indexPath, indexPage.html); index = { ...indexPage, path: "index.html" }; } return { pages, index: index || void 0 }; } catch (error) { if (error instanceof StaticGeneratorError) { throw error; } throw new StaticGeneratorError( `Static site generation failed: ${error instanceof Error ? error.message : "Unknown error"}`, contentPath ); } } function generateIndexPage(navigation, options) { const title = options.indexTitle || options.title || "Documentation"; const baseUrl = options.baseUrl || ""; const css = options.css || ""; const js = options.js || ""; const navigationHtml = generateNavigationHtml(navigation.items); const html = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${title}</title> <base href="${baseUrl}"> ${css ? `<style>${css}</style>` : ""} </head> <body> <div class="container"> <header> <h1>${title}</h1> </header> <nav class="navigation"> ${navigationHtml} </nav> <main class="content"> <p>Welcome to the documentation. Please select a page from the navigation.</p> </main> </div> ${js ? `<script>${js}</script>` : ""} </body> </html>`; return { html }; } function generateNavigationHtml(items) { let html = '<ul class="nav-list">'; for (const item of items) { html += '<li class="nav-item">'; if (item.type === "page" && item.path) { const href = item.path.replace(/\.md$/, ".html"); html += `<a href="${href}" class="nav-link">${item.label}</a>`; } else { html += `<span class="nav-section">${item.label}</span>`; } if (item.items && item.items.length > 0) { html += generateNavigationHtml(item.items); } html += "</li>"; } html += "</ul>"; return html; } function generateSitemap(pages, baseUrl) { const sitemap = `<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> ${pages.map((page) => { const url = `${baseUrl}/${page.path}`; return ` <url> <loc>${url}</loc> <lastmod>${(/* @__PURE__ */ new Date()).toISOString()}</lastmod> </url>`; }).join("\n")} </urlset>`; return sitemap; } function generateRobotsTxt(baseUrl) { return `User-agent: * Allow: / Sitemap: ${baseUrl}/sitemap.xml`; } exports.StaticGeneratorError = StaticGeneratorError; exports.generateRobotsTxt = generateRobotsTxt; exports.generateSitemap = generateSitemap; exports.generateStaticSite = generateStaticSite; //# sourceMappingURL=static-generator.cjs.map //# sourceMappingURL=static-generator.cjs.map