UNPKG

svelte-markdown-pages

Version:

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

343 lines (340 loc) 11.2 kB
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