Files
STCourses/packages/course-format/src/repository.ts
T

43 lines
1.7 KiB
TypeScript

import { extname } from "node:path";
import { loadAll, resolveInside, type LoadedCourse } from "./load";
import { extractSection } from "./markdown";
export interface CourseRepository {
list(): LoadedCourse[];
get(id: string): LoadedCourse | null;
/** Re-scans the course dirs. `id` is accepted for API symmetry; a rescan is cheap and keeps overlay shadowing correct. */
reload(id?: string): Promise<void>;
readLessonMarkdown(courseId: string, lessonId: string): Promise<string | null>;
/** Absolute real path if inside the course root (symlinks followed), else null. */
resolvePath(courseId: string, relPath: string): string | null;
}
const MD = new Set([".md", ".markdown"]);
export async function createCourseRepository(courseDirs: string[]): Promise<CourseRepository> {
let courses = await loadAll(courseDirs);
const repo: CourseRepository = {
list: () => courses,
get: (id) => courses.find((c) => c.id === id) ?? null,
async reload() {
courses = await loadAll(courseDirs);
},
async readLessonMarkdown(courseId, lessonId) {
const entry = repo.get(courseId)?.lessonIndex.get(lessonId);
if (!entry) return null;
const { lesson } = entry;
const rel = lesson.type === "video" ? lesson.transcript : lesson.file;
if (!rel || !MD.has(extname(rel).toLowerCase())) return null;
const abs = repo.resolvePath(courseId, rel);
if (!abs) return null;
const text = await Bun.file(abs).text();
return lesson.anchor ? extractSection(text, lesson.anchor) : text;
},
resolvePath(courseId, relPath) {
const c = repo.get(courseId);
return c ? resolveInside(c.root, relPath) : null;
},
};
return repo;
}