import { DataAdapter, DataWriteOptions, EventRef, TAbstractFile, TFile, TFolder, Vault, } from "obsidian"; import { basename, dirname, join, normalize } from "path"; /** * Return all files that exist under a given folder. * @param folder Folder to collect children under. * @returns All files under this folder, recursively. */ const collectChildren = (folder: TFolder): TAbstractFile[] => { return folder.children.flatMap((f) => { if (f instanceof TFolder) { return [f, ...collectChildren(f)]; } else { return f; } }); }; export class MockVault implements Vault { root: TFolder; contents: Map; constructor(root: TFolder, contents: Map) { this.root = root; this.contents = contents; } // These aren't implemented in the mock. adapter: DataAdapter = {} as DataAdapter; configDir = ""; getName(): string { return "Mock Vault"; } getAllLoadedFiles(): TAbstractFile[] { return [this.root, ...collectChildren(this.root)]; } getAbstractFileByPath(path: string): TAbstractFile | null { const normalizedPath = join("/", normalize(path)); return ( this.getAllLoadedFiles().find( (f) => join("/", normalize(f.path)) === normalizedPath ) || null ); } getRoot(): TFolder { return this.root; } async read(file: TFile): Promise { const p = join("/", file.path); const contents = this.contents.get(p); if (!contents) { throw new Error(`File at path ${p} does not have contents`); } return contents; } cachedRead(file: TFile): Promise { return this.read(file); } getFiles(): TFile[] { return this.getAllLoadedFiles().flatMap((f) => f instanceof TFile ? f : [] ); } getMarkdownFiles(): TFile[] { return this.getFiles().filter( (f) => f.extension.toLowerCase() === "md" ); } private setParent(path: string, f: TAbstractFile) { const parentPath = dirname(path); const folder = this.getAbstractFileByPath(parentPath); if (folder instanceof TFolder) { f.parent = folder; folder.children.push(f); } throw new Error("Parent path is not folder."); } process( file: TFile, fn: (data: string) => string, options?: DataWriteOptions | undefined ): Promise { throw new Error("Method not implemented."); } async create( path: string, data: string, options?: DataWriteOptions | undefined ): Promise { if (this.getAbstractFileByPath(path)) { throw new Error("File already exists."); } const file = new TFile(); file.name = basename(path); this.setParent(path, file); this.contents.set(path, data); return file; } async createFolder(path: string): Promise { const folder = new TFolder(); folder.name = basename(path); this.setParent(path, folder); return folder; } async delete( file: TAbstractFile, force?: boolean | undefined ): Promise { file.parent?.children.remove(file); } trash(file: TAbstractFile, system: boolean): Promise { return this.delete(file); } async rename(file: TAbstractFile, newPath: string): Promise { const newParentPath = dirname(newPath); const newParent = this.getAbstractFileByPath(newParentPath); if (!(newParent instanceof TFolder)) { throw new Error(`No such folder: ${newParentPath}`); } if (file instanceof TFile) { // If we're renaming a file, just update the parent and name in the // file, and the entry in the content map. const contents = this.contents.get(file.path); if (!contents) { throw new Error(`File did not have contents: ${file.path}`); } this.contents.delete(file.path); // Update the parent and name and re-set contents with the new path. // NOTE: This relies on using the included mock that derives the path // from the parent and filename as a getter property. file.parent = newParent; file.name = basename(newPath); this.contents.set(file.path, contents); } else if (file instanceof TFolder) { // If we're renaming a folder, we need to update the content map for // every TFile under this folder. // Collect all files under this folder, get the string contents, delete // the entry for the old path, and return the file and contents in a tuple. const filesAndContents = collectChildren(file) .flatMap((f) => (f instanceof TFile ? f : [])) .map((f): [TFile, string] => { const contents = this.contents.get(f.path); if (!contents) { throw new Error( `File did not have contents: ${f.path}` ); } this.contents.delete(f.path); return [f, contents]; }); // Update the parent and name for this folder. file.parent = newParent; file.name = basename(newPath); // Re-add all the paths to the content dir. for (const [f, contents] of filesAndContents) { this.contents.set(f.path, contents); } } else { throw new Error(`File is not a file or folder: ${file.path}`); } } async modify( file: TFile, data: string, options?: DataWriteOptions | undefined ): Promise { this.contents.set(file.path, data); } async copy(file: TFile, newPath: string): Promise { const data = await this.read(file); return await this.create(newPath, data); } // TODO: Implement callbacks. on( name: "create", callback: (file: TAbstractFile) => unknown, ctx?: unknown ): EventRef; on( name: "modify", callback: (file: TAbstractFile) => unknown, ctx?: unknown ): EventRef; on( name: "delete", callback: (file: TAbstractFile) => unknown, ctx?: unknown ): EventRef; on( name: "rename", callback: (file: TAbstractFile, oldPath: string) => unknown, ctx?: unknown ): EventRef; on(name: "closed", callback: () => unknown, ctx?: unknown): EventRef; on(name: unknown, callback: unknown, ctx?: unknown): EventRef { throw new Error("Method not implemented."); } off(name: string, callback: (...data: unknown[]) => unknown): void { throw new Error("Method not implemented."); } offref(ref: EventRef): void { throw new Error("Method not implemented."); } trigger(name: string, ...data: unknown[]): void { throw new Error("Method not implemented."); } tryTrigger(evt: EventRef, args: unknown[]): void { throw new Error("Method not implemented."); } append( file: TFile, data: string, options?: DataWriteOptions | undefined ): Promise { throw new Error("Method not implemented."); } createBinary( path: string, data: ArrayBuffer, options?: DataWriteOptions | undefined ): Promise { throw new Error("Method not implemented."); } readBinary(file: TFile): Promise { throw new Error("Method not implemented."); } modifyBinary( file: TFile, data: ArrayBuffer, options?: DataWriteOptions | undefined ): Promise { throw new Error("Method not implemented."); } getResourcePath(file: TFile): string { throw new Error("Method not implemented."); } }