import path from "node:path"; import { globSync } from "glob"; import MarkdownDocument from "./document.js"; /** * Load documents from an Obsidian vault * @param vaultPath The path to the vault to load * @param ignorePatterns A glob pattern to ignore files for * @returns A promise which resolves as an array of laoded Documents */ function loadVaultDocuments( vault: Vault, ignorePatterns: string[] = [], ): MarkdownDocument[] { const discoveredMarkdownDocuments = globSync(`${vault.vaultPath}/**/*.md`, { ignore: ignorePatterns, }); const markdownDocuments: MarkdownDocument[] = []; for (const filePath of discoveredMarkdownDocuments) { const file = new MarkdownDocument(filePath, vault); if (markdownDocuments.some((check) => check.slug === file.slug)) { throw new Error("Duplicate slug: " + file.slug); } markdownDocuments.push(file); } return markdownDocuments; } /** * Build a VaultIndex from a list of loaded Documents * @param vaultDocs An array of all the documents to index * @returns A VaultIndex, which is an object that maps document slugs to information */ function buildVaultIndex( vaultDocs: MarkdownDocument[], ): Record { const index: Record = {}; for (const document of vaultDocs) { index[document.slug] = document; } return index; } class VaultView { /** An array of all discovered Markdown documents */ documents: MarkdownDocument[] = []; /** An array of all generated document slugs */ slugs: string[] = []; /** A map of generated document slugs to their associated document */ index: Record = {}; /** The absolute path of this fault on disk */ readonly vault: Vault; /** The number of documents in this vault */ readonly size: number = 0; constructor(documents: MarkdownDocument[], vault: Vault) { this.documents = documents; this.slugs = documents.map((doc) => doc.slug); this.index = buildVaultIndex(documents); this.vault = vault; this.size = documents.length; } /** * Map over each document in this view, modifying it as needed * @param fn A function to map over every document in this view * @returns A reference to this view to chain additional calls */ process(fn: (document: MarkdownDocument) => void): VaultView { this.documents.forEach(fn); return this; } /** * Utility function to chain function calls back to the original vault * @returns A reference to the vault that produced this view */ unscope(): Vault { return this.vault; } } interface VaultOptions { ignorePatterns?: string[]; } export default class Vault { /** An array of all discovered Markdown documents */ documents: MarkdownDocument[] = []; /** An array of all generated document slugs */ slugs: string[] = []; /** A map of generated document slugs to their associated document */ index: Record = {}; /** The absolute path of this fault on disk */ readonly vaultPath: string; /** The number of documents in this vault */ readonly size: number = 0; /** File patterns to ignore when discovering vault files */ private ignorePatterns: string[] = []; constructor(vaultRootPath: string, options?: VaultOptions) { this.vaultPath = path.resolve(vaultRootPath); this.ignorePatterns = options?.ignorePatterns ?? []; const allFiles = loadVaultDocuments(this, this.ignorePatterns); const allSlugs = allFiles.map((doc) => doc.slug); this.documents = allFiles; this.slugs = allSlugs; this.size = allFiles.length; this.index = buildVaultIndex(allFiles); } scope(fn: (document: MarkdownDocument) => boolean): VaultView { const matchingDocs = this.documents.filter(fn); return new VaultView(matchingDocs, this); } /** * Map over each document in this vault, modifying it as needed * @param fn A function to map over every document in this vault * @returns A reference to this vault to chain additional calls */ process(fn: (document: MarkdownDocument) => void): Vault { this.documents.forEach(fn); return this; } write(): Vault { this.documents.forEach((doc) => { doc.write(); }); return this; } }