Generic muse

This commit is contained in:
Endeavorance 2025-04-01 15:43:48 -04:00
parent c66dd4d39c
commit c1166680a8
31 changed files with 412 additions and 2221 deletions

168
src/muse-file.ts Normal file
View file

@ -0,0 +1,168 @@
import type { BunFile } from "bun";
import { normalize, basename, extname, dirname } from "node:path";
import YAML from "yaml";
import TOML from "smol-toml";
import { FileError } from "./errors";
export type MuseFileType = "md" | "yaml" | "json" | "toml";
export interface MuseFileContent {
type: MuseFileType;
text: string;
data: Record<string, unknown>;
}
function extensionToFiletype(ext: string): MuseFileType {
switch (ext) {
case "md":
return "md";
case "yaml":
return "yaml";
case "json":
return "json";
case "toml":
return "toml";
default:
throw new Error(`Unsupported file format: ${ext}`);
}
}
const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm;
/**
* Attempt to parse YAML frontmatter from a mixed yaml/md doc
* @param content The raw markdown content
* @returns Any successfully parsed frontmatter
*/
function extractFrontmatter(content: string) {
// If it does not start with `---`, it is invalid for frontmatter
if (content.trim().indexOf("---") !== 0) {
return {};
}
if (FRONTMATTER_REGEX.test(content)) {
const frontmatterString = content.match(FRONTMATTER_REGEX)?.[0] ?? "";
const cleanFrontmatter = frontmatterString.replaceAll("---", "").trim();
return YAML.parse(cleanFrontmatter);
}
return {};
}
/**
* Given a string of a markdown document, extract the markdown content
* @param content The raw markdown content
* @returns The markdown content without frontmatter
*/
function extractMarkdown(content: string): string {
if (content.trim().indexOf("---") !== 0) {
return content;
}
return content.replace(FRONTMATTER_REGEX, "").trim();
}
export class MuseFile {
protected _path: string;
protected _file: BunFile;
protected _fileType: MuseFileType;
constructor(filePath: string) {
this._path = normalize(filePath);
this._file = Bun.file(this._path);
this._fileType = extensionToFiletype(this.extension.slice(1));
}
get dirname(): string {
return dirname(this._path);
}
get extension(): string {
return extname(this._path);
}
get basename(): string {
return basename(this._path);
}
get filename(): string {
return basename(this._path, this.extension);
}
get path(): string {
return this._path;
}
public async readJSON(): Promise<MuseFileContent> {
try {
const text = await this._file.text();
return {
type: "json",
text: text,
data: JSON.parse(text),
};
} catch (error) {
throw new FileError(`Failed to read JSON file: ${error}`, this._path);
}
}
public async readYAML(): Promise<MuseFileContent> {
try {
const text = await this._file.text();
return {
type: "yaml",
text,
data: YAML.parse(text),
};
} catch (error) {
throw new FileError(`Failed to read YAML file: ${error}`, this._path);
}
}
public async readTOML(): Promise<MuseFileContent> {
try {
const text = await this._file.text();
return {
type: "toml",
text,
data: TOML.parse(text),
};
} catch (error) {
throw new FileError(`Failed to read TOML file: ${error}`, this._path);
}
}
public async readMarkdown(): Promise<MuseFileContent> {
try {
const text = await this._file.text();
const frontmatter = extractFrontmatter(text);
const markdown = extractMarkdown(text);
return {
type: "md",
text: markdown,
data: {
...frontmatter,
},
};
} catch (error) {
throw new FileError(`Failed to read Markdown file: ${error}`, this._path);
}
}
public async read(): Promise<MuseFileContent> {
switch (this._fileType) {
case "json":
return this.readJSON();
case "yaml":
return this.readYAML();
case "toml":
return this.readTOML();
case "md":
return this.readMarkdown();
default:
throw new FileError(
`No reader for file type ${this._fileType}`,
this._path,
);
}
}
}