import { existsSync, readFileSync, writeFileSync } from "fs"; import path from "node:path"; import slug from "slug"; import type Vault from "./vault.js"; import { combineMarkdownAndFrontmatter, extractFrontmatter, extractMarkdown, extractTags, type FrontmatterShape, } from "./markdown.js"; export default class MarkdownDocument { /** A reference to the vault containing this document */ readonly vault: Vault; /** The original file path to this document */ readonly path: string; /** The directory/folder that this Document is in, relative to the vault root */ readonly dirname: string; /** An array of directory names this document is found under */ readonly taxonomy: string[]; /** The name of this document's file, without the file extension */ readonly filename: string; /** An array of tags found in this document from both the markdown and frontmatter */ tags: string[]; /** A url-safe version of this file's name */ readonly slug: string; /** The raw content of the file */ private _content: string; /** The history of this files contents after each modification */ readonly contentHistory: string[]; /** The parsed frontmatter of the file */ private _frontmatter: FrontmatterShape; /** Structured document metadata */ meta: Record = {}; /** The markdown portion of the file (without frontmatter) */ private _markdown: string; constructor(filePath: string, vault: Vault) { if (!existsSync(filePath)) { throw new Error(`File not found: ${filePath}`); } const rawFileContent = readFileSync(filePath, "utf-8"); const fileDirname = path.dirname(filePath); const vaultPath = vault.vaultPath; const frontmatter = extractFrontmatter(rawFileContent); this.path = filePath; this.dirname = path.relative(vaultPath, fileDirname); this.taxonomy = this.dirname.split(path.sep); this.filename = path.basename(filePath, ".md"); this.slug = slug(`${this.taxonomy.join("-")}-${this.filename}`); this._content = rawFileContent; this.contentHistory = [rawFileContent]; this._frontmatter = frontmatter; this._markdown = extractMarkdown(rawFileContent); this.vault = vault; const frontmatterTags = frontmatter.tags; const contentTags = extractTags(this._markdown); this.tags = frontmatterTags.concat(contentTags); } /** * @returns A serializable shape for this object */ toJSON() { return { path: this.path, dirname: this.dirname, taxonomy: this.taxonomy, filename: this.filename, slug: this.slug, frontmatter: this._frontmatter, markdown: this._markdown, }; } /** * Update the markdown content for the document * @param newValue The new markdown content for the document */ setMarkdown(newValue: string): MarkdownDocument { this._markdown = newValue; this._content = combineMarkdownAndFrontmatter(newValue, this.frontmatter); this.contentHistory.push(this._content); return this; } /** * Update the frontmatter of the document * @param newValue The new frontmatter shape for this document */ setFrontmatter(newValue: FrontmatterShape): MarkdownDocument { this._frontmatter = newValue; this._content = combineMarkdownAndFrontmatter(this._markdown, newValue); this.contentHistory.push(this._content); return this; } /** * Update the full content of the doc with a combination of markdown and yaml frontmatter * @param newValue The full content of the document to set */ setContent(newValue: string): MarkdownDocument { this._content = newValue; this._frontmatter = extractFrontmatter(newValue); this._markdown = extractMarkdown(newValue); this.contentHistory.push(this._content); return this; } /** * Check if this document has a specific tag * @param tag The tag to look for * @returns `true` If this document has the given tag */ hasTag(tag: string) { return this.tags.includes(tag); } /** * Given an array of directory names, return `true` if this document has the taxonomy * @param dirs Directories to check for in the taxonomy * @returns `true` If this document shares the given taxonomy */ hasTaxonomy(dirs: string[]) { if (dirs.length > this.taxonomy.length) { return false; } for (let i = 0; i < dirs.length; i++) { if (dirs[i] !== this.taxonomy[i]) { return false; } } return true; } /** * Revert this document to its original loaded content */ revert(): MarkdownDocument { const originalCopy = this.contentHistory[0]; if (originalCopy === undefined) { throw new Error("Document has no history to revert to."); } this.setContent(originalCopy); return this; } /** * Write this document back to disk */ write(): MarkdownDocument { writeFileSync( this.path, combineMarkdownAndFrontmatter(this._markdown, this.frontmatter), "utf-8", ); return this; } /** The markdown portion of the file (without frontmatter) */ get markdown() { return this._markdown; } /** The parsed frontmatter of the file */ get frontmatter() { return this._frontmatter; } /** The raw content of the file */ get content() { return this._content; } }