hammerstone/src/vault.ts

153 lines
4.2 KiB
TypeScript

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<string, MarkdownDocument> {
const index: Record<string, MarkdownDocument> = {};
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<string, MarkdownDocument> = {};
/** 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<string, MarkdownDocument> = {};
/** 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;
}
}