51 lines
2.7 KiB
TypeScript
51 lines
2.7 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import catalog from "../../../../content/catalog.json";
|
|
|
|
const directory = path.resolve(process.cwd(), "../../content/docs");
|
|
const validSlug = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*$/;
|
|
type Metadata = { slug?: string; category?: string; collection?: string; group?: string; description?: string; draft?: boolean; featured?: boolean };
|
|
export type DocLink = {
|
|
slug: string; sourceSlug: string; category: string; title: string; searchText?: string;
|
|
group: string; collection: string; description: string; draft: boolean; featured: boolean;
|
|
};
|
|
export type Doc = DocLink & { content: string };
|
|
|
|
export async function readDoc(slug: string): Promise<Doc> {
|
|
if (!validSlug.test(slug)) throw new Error("Invalid document slug");
|
|
const sourceSlug = Object.entries(catalog.documents).find(([, metadata]) => metadata.slug === slug)?.[0] ?? slug;
|
|
const content = await fs.readFile(path.join(directory, `${sourceSlug}.md`), "utf8");
|
|
const title = content.match(/^# (.+)$/m)?.[1]?.trim() ?? slug;
|
|
const collection = catalog.collections.find((c) => !c.path || sourceSlug.startsWith(`${c.path}/`))!;
|
|
const metadata: Metadata = (catalog.documents as Record<string, Metadata>)[sourceSlug] ?? {};
|
|
if (!metadata.slug || !metadata.category || !validSlug.test(metadata.slug) || metadata.slug.split("/").some((part) => /^\d+-/.test(part))) {
|
|
throw new Error(`Register a semantic slug and category in content/catalog.json: ${sourceSlug}`);
|
|
}
|
|
return { slug: metadata.slug, sourceSlug, category: metadata.category, title, content, searchText: content,
|
|
group: metadata.group ?? collection.title, collection: metadata.collection ?? collection.title,
|
|
description: metadata.description ?? collection.description,
|
|
draft: metadata.draft ?? false, featured: metadata.featured ?? false,
|
|
};
|
|
}
|
|
|
|
export async function listDocs(): Promise<DocLink[]> {
|
|
async function walk(folder: string): Promise<string[]> {
|
|
const entries = await fs.readdir(path.join(directory, folder), { withFileTypes: true });
|
|
const files = await Promise.all(entries.map(async (entry) => {
|
|
const relative = folder ? `${folder}/${entry.name}` : entry.name;
|
|
if (entry.isDirectory()) return walk(relative);
|
|
if (entry.isFile() && relative.endsWith(".md") && validSlug.test(relative.slice(0, -3)))
|
|
return [relative.slice(0, -3)];
|
|
return [];
|
|
}));
|
|
return files.flat();
|
|
}
|
|
const docs = await Promise.all((await walk("")).sort().map(async (slug) => {
|
|
const { content: _, searchText: __, ...doc } = await readDoc(slug);
|
|
void _; void __;
|
|
return doc;
|
|
}));
|
|
if (new Set(docs.map((doc) => doc.slug)).size !== docs.length) throw new Error("Duplicate public document slug");
|
|
return docs;
|
|
}
|