svelte-markdown-pages
Version:
Build and render markdown-based content with distributed navigation for Svelte projects
618 lines (612 loc) • 20 kB
JavaScript
var fs = require('fs');
var path = require('path');
var marked = require('marked');
var zod = require('zod');
// src/builder/builder.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
);
}
}
// src/builder/builder.ts
var BuilderError = class extends Error {
constructor(message, filePath) {
super(message);
this.filePath = filePath;
this.name = "BuilderError";
}
};
async function buildPages(contentPath, options = {}) {
try {
validateContentStructure(contentPath, { autoDiscover: options.autoDiscover });
const navigation = buildNavigationTree(contentPath, { autoDiscover: options.autoDiscover });
let content;
if (options.includeContent !== false) {
content = bundleMarkdownContent(navigation, contentPath);
}
if (options.appOutput) {
await writeAppOutput(navigation, content, options.appOutput);
}
if (options.websiteOutput) {
await writeWebsiteOutput(navigation, options.websiteOutput);
}
return {
navigation,
content: content || void 0
};
} catch (error) {
if (error instanceof BuilderError) {
throw error;
}
throw new BuilderError(
`Build failed: ${error instanceof Error ? error.message : "Unknown error"}`,
contentPath
);
}
}
function bundleMarkdownContent(navigation, basePath) {
const content = {};
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");
content[item.path] = markdownContent;
} catch (error) {
throw new BuilderError(
`Failed to read markdown file: ${error instanceof Error ? error.message : "Unknown error"}`,
filePath
);
}
} else if (item.items) {
processItems(item.items);
}
}
}
processItems(navigation.items);
return content;
}
async function writeAppOutput(navigation, content, outputPath) {
try {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
const navigationPath = path.join(outputPath, "navigation.json");
fs.writeFileSync(navigationPath, JSON.stringify(navigation, null, 2));
if (content) {
const contentPath = path.join(outputPath, "content.json");
fs.writeFileSync(contentPath, JSON.stringify(content, null, 2));
}
} catch (error) {
throw new BuilderError(
`Failed to write app output: ${error instanceof Error ? error.message : "Unknown error"}`,
outputPath
);
}
}
async function writeWebsiteOutput(navigation, outputPath) {
try {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
const navigationPath = path.join(outputPath, "navigation.json");
fs.writeFileSync(navigationPath, JSON.stringify(navigation, null, 2));
} catch (error) {
throw new BuilderError(
`Failed to write website output: ${error instanceof Error ? error.message : "Unknown error"}`,
outputPath
);
}
}
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>`;
}
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.BuilderError = BuilderError;
exports.ParserError = ParserError;
exports.StaticGeneratorError = StaticGeneratorError;
exports.buildNavigationTree = buildNavigationTree;
exports.buildPages = buildPages;
exports.generateRobotsTxt = generateRobotsTxt;
exports.generateSitemap = generateSitemap;
exports.generateStaticPages = generateStaticPages;
exports.generateStaticSite = generateStaticSite;
exports.parseIndexFile = parseIndexFile;
exports.processMarkdown = processMarkdown;
exports.validateContentStructure = validateContentStructure;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map
;