hammerstone/src/document.ts
2024-05-12 08:54:40 -04:00

195 lines
5.2 KiB
TypeScript

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<string, unknown> = {};
/** 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;
}
}