svelte-markdown-pages
Version:
Build and render markdown-based content with distributed navigation for Svelte projects
343 lines (340 loc) • 11.2 kB
JavaScript
import { readFileSync, statSync, existsSync, readdirSync } from 'fs';
import { join, relative } from 'path';
import { z } from 'zod';
// src/builder/parser.ts
var DocItemTypeSchema = z.enum(["section", "page"]);
var DocItemSchema = z.object({
name: z.string().min(1),
type: DocItemTypeSchema,
label: z.string().min(1),
collapsed: z.boolean().optional(),
url: z.string().url().optional()
});
var IndexSchema = z.object({
items: 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 = 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 = 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 = join(dirPath, ".index.json");
return statSync(indexPath).isFile();
} catch {
return false;
}
}
function buildNavigationTree(contentPath, options = {}) {
const { basePath = contentPath, validateFiles = true, autoDiscover = true } = options;
try {
if (!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 = 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 = join(currentPath, item.name);
if (hasIndexFile(sectionPath)) {
const sectionIndexPath = join(sectionPath, ".index.json");
if (validateFiles) {
try {
if (!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 = join(sectionPath, ".index.json");
throw new ParserError(
`Section .index.json not found: ${sectionIndexPath}`,
sectionIndexPath
);
}
const indexPath = join(sectionPath, "index.md");
const readmePath = join(sectionPath, "README.md");
const readmeLowerPath = join(sectionPath, "readme.md");
let sectionRootPath;
if (validateFiles) {
try {
if (statSync(indexPath).isFile()) {
sectionRootPath = indexPath;
}
} catch {
try {
if (statSync(readmePath).isFile()) {
sectionRootPath = readmePath;
}
} catch {
try {
if (statSync(readmeLowerPath).isFile()) {
sectionRootPath = readmeLowerPath;
}
} catch {
}
}
}
} else {
if (existsSync(indexPath)) {
sectionRootPath = indexPath;
} else if (existsSync(readmePath)) {
sectionRootPath = readmePath;
} else if (existsSync(readmeLowerPath)) {
sectionRootPath = readmeLowerPath;
}
}
if (sectionRootPath) {
navigationItem.path = relative(basePath, sectionRootPath);
}
} else if (item.type === "page") {
const pagePath = join(currentPath, `${item.name}.md`);
const relativePath = relative(basePath, pagePath);
if (validateFiles) {
try {
if (!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 = join(dirPath, ".index.json");
if (hasIndexFile(dirPath)) {
try {
const items = parseIndexFile(indexPath);
for (const item of items) {
if (item.type === "section") {
const sectionPath = join(dirPath, item.name);
try {
if (!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 = join(dirPath, `${item.name}.md`);
try {
if (!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 = join(dirPath, item.name);
try {
if (!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 = join(dirPath, `${item.name}.md`);
try {
if (!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
);
}
}
export { ParserError, buildNavigationTree, parseIndexFile, validateContentStructure };
//# sourceMappingURL=parser.js.map
//# sourceMappingURL=parser.js.map