Move catalog indexes to Markdown front matter
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { parseDocument } from "yaml";
|
||||
|
||||
export type CatalogIndex<T> = {
|
||||
description: string;
|
||||
metadata: T;
|
||||
};
|
||||
|
||||
export function parseCatalogIndex<T>(source: string, indexPath: string): CatalogIndex<T> {
|
||||
const lines = source.split(/\r?\n/);
|
||||
if (lines[0] !== "---") {
|
||||
throw new Error(`${indexPath}: YAML Front Matter must start with ---`);
|
||||
}
|
||||
|
||||
const closingDelimiter = lines.indexOf("---", 1);
|
||||
if (closingDelimiter === -1) {
|
||||
throw new Error(`${indexPath}: YAML Front Matter is missing its closing ---`);
|
||||
}
|
||||
|
||||
const document = parseDocument(lines.slice(1, closingDelimiter).join("\n"), {
|
||||
prettyErrors: true,
|
||||
});
|
||||
if (document.errors.length > 0) {
|
||||
throw new Error(
|
||||
`${indexPath}: ${document.errors.map((error) => error.message).join("; ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const metadata = document.toJS() as unknown;
|
||||
if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
|
||||
throw new Error(`${indexPath}: YAML Front Matter must contain an object`);
|
||||
}
|
||||
if (Object.hasOwn(metadata, "description")) {
|
||||
throw new Error(`${indexPath}: description must be the Markdown body`);
|
||||
}
|
||||
|
||||
const description = lines.slice(closingDelimiter + 1).join("\n").trim();
|
||||
if (description.length === 0) {
|
||||
throw new Error(`${indexPath}: Markdown description is missing`);
|
||||
}
|
||||
|
||||
return { metadata: metadata as T, description };
|
||||
}
|
||||
|
||||
export async function readCatalogIndex<T>(indexPath: string): Promise<CatalogIndex<T>> {
|
||||
return parseCatalogIndex<T>(await readFile(indexPath, "utf8"), indexPath);
|
||||
}
|
||||
+6
-10
@@ -15,7 +15,8 @@ import {
|
||||
} from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { parseDocument, stringify } from "yaml";
|
||||
import { stringify } from "yaml";
|
||||
import { readCatalogIndex } from "./catalogIndex.js";
|
||||
|
||||
type CatalogFile = {
|
||||
path?: unknown;
|
||||
@@ -91,7 +92,7 @@ async function findIndexes(directory: string, result: string[]): Promise<void> {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(directory, entry.name);
|
||||
if (entry.isFile() && entry.name === "index.yaml") {
|
||||
if (entry.isFile() && entry.name === "index.md") {
|
||||
result.push(entryPath);
|
||||
} else if (entry.isDirectory() && !ignoredDirectories.has(entry.name)) {
|
||||
await findIndexes(entryPath, result);
|
||||
@@ -182,12 +183,7 @@ async function writeAtomically(filePath: string, contents: string): Promise<void
|
||||
}
|
||||
|
||||
async function readProgram(indexPath: string): Promise<ProgramIndex> {
|
||||
const source = await readFile(indexPath, "utf8");
|
||||
const document = parseDocument(source, { prettyErrors: true });
|
||||
if (document.errors.length > 0) {
|
||||
throw new Error(document.errors.map((error) => error.message).join("; "));
|
||||
}
|
||||
return document.toJS() as ProgramIndex;
|
||||
return (await readCatalogIndex<ProgramIndex>(indexPath)).metadata;
|
||||
}
|
||||
|
||||
async function generateManifest(catalogDirectory: string): Promise<{
|
||||
@@ -275,7 +271,7 @@ async function main(): Promise<void> {
|
||||
const displayPath = path.relative(process.cwd(), manifestPath) || manifestPath;
|
||||
if (currentContents === contents) {
|
||||
console.log(
|
||||
`Done. ${displayPath} is up to date. Files: ${Object.keys(manifest.files).length}; index.yaml files: ${indexes}`,
|
||||
`Done. ${displayPath} is up to date. Files: ${Object.keys(manifest.files).length}; index.md files: ${indexes}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -288,7 +284,7 @@ async function main(): Promise<void> {
|
||||
|
||||
await writeAtomically(manifestPath, contents);
|
||||
console.log(
|
||||
`Updated ${displayPath}. Files: ${Object.keys(manifest.files).length}; index.yaml files: ${indexes}`,
|
||||
`Updated ${displayPath}. Files: ${Object.keys(manifest.files).length}; index.md files: ${indexes}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+8
-23
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { lstat, readdir, readFile } from "node:fs/promises";
|
||||
import { lstat, readdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { parseDocument } from "yaml";
|
||||
import { readCatalogIndex } from "./catalogIndex.js";
|
||||
|
||||
type CatalogType = "category" | "program";
|
||||
|
||||
@@ -43,7 +43,7 @@ function usage(): void {
|
||||
console.log(`Usage: pnpm tree [options] [path]
|
||||
|
||||
With no path, the script prints the complete tree from catalog. A path may point
|
||||
to a category directory, a program directory, or its index.yaml file.
|
||||
to a category directory, a program directory, or its index.md file.
|
||||
|
||||
--versions show versions under each program
|
||||
--paths show relative directory paths
|
||||
@@ -98,30 +98,15 @@ async function resolveDirectory(target: string): Promise<string> {
|
||||
if (targetStat.isDirectory()) {
|
||||
return absoluteTarget;
|
||||
}
|
||||
if (targetStat.isFile() && path.basename(absoluteTarget) === "index.yaml") {
|
||||
if (targetStat.isFile() && path.basename(absoluteTarget) === "index.md") {
|
||||
return path.dirname(absoluteTarget);
|
||||
}
|
||||
throw new Error(`Expected a catalog directory or index.yaml file: ${target}`);
|
||||
throw new Error(`Expected a catalog directory or index.md file: ${target}`);
|
||||
}
|
||||
|
||||
async function readCatalogNode(directory: string): Promise<CatalogNode> {
|
||||
const indexPath = path.join(directory, "index.yaml");
|
||||
let source: string;
|
||||
|
||||
try {
|
||||
source = await readFile(indexPath, "utf8");
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`${indexPath}: failed to read index.yaml: ${reason}`);
|
||||
}
|
||||
|
||||
const document = parseDocument(source, { prettyErrors: true });
|
||||
if (document.errors.length > 0) {
|
||||
const errors = document.errors.map((error) => error.message).join("; ");
|
||||
throw new Error(`${indexPath}: ${errors}`);
|
||||
}
|
||||
|
||||
const data = document.toJS() as CatalogIndex;
|
||||
const indexPath = path.join(directory, "index.md");
|
||||
const { metadata: data } = await readCatalogIndex<CatalogIndex>(indexPath);
|
||||
if (data.type !== "category" && data.type !== "program") {
|
||||
throw new Error(`${indexPath}: unknown type`);
|
||||
}
|
||||
@@ -161,7 +146,7 @@ async function readCatalogNode(directory: string): Promise<CatalogNode> {
|
||||
|
||||
const childDirectory = path.join(directory, entry.name);
|
||||
try {
|
||||
const childIndexStat = await lstat(path.join(childDirectory, "index.yaml"));
|
||||
const childIndexStat = await lstat(path.join(childDirectory, "index.md"));
|
||||
if (childIndexStat.isFile()) {
|
||||
children.push(await readCatalogNode(childDirectory));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user