243 lines
6.8 KiB
TypeScript
243 lines
6.8 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.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;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the value of a frontmatter key as a string.
|
|
* If the key is not found, returns the provided default value.
|
|
* Throws an error if the value is not a string.
|
|
*
|
|
* @param key - The key of the frontmatter value to retrieve.
|
|
* @param defaultValue - The default value to return if the key is not found.
|
|
* @returns The value of the frontmatter key as a string.
|
|
* @throws Error if the frontmatter key is not a string.
|
|
*/
|
|
getFrontmatterString(key: string, defaultValue: string): string {
|
|
const val = this._frontmatter[key];
|
|
|
|
if (val === undefined) {
|
|
return defaultValue;
|
|
}
|
|
|
|
if (typeof val !== "string") {
|
|
throw new Error(`Frontmatter key is not a string: ${key}`);
|
|
}
|
|
|
|
return val;
|
|
}
|
|
|
|
/**
|
|
* Retrieves an array value from the frontmatter object based on the provided key.
|
|
* If the value is not found, it returns the provided defaultValue.
|
|
* If the value is found but is not an array, it throws an error.
|
|
*
|
|
* @param key - The key to retrieve the array value from the frontmatter object.
|
|
* @param defaultValue - The default value to return if the key is not found in the frontmatter object.
|
|
* @returns The array value associated with the provided key, or the defaultValue if the key is not found.
|
|
* @throws Error if the value associated with the key is found but is not an array.
|
|
*/
|
|
getFrontmatterArray(key: string, defaultValue: string[]): string[] {
|
|
const val = this._frontmatter[key];
|
|
|
|
if (val === undefined) {
|
|
return defaultValue;
|
|
}
|
|
|
|
if (!Array.isArray(val)) {
|
|
throw new Error(`Frontmatter key is not an array: ${key}`);
|
|
}
|
|
|
|
return val;
|
|
}
|
|
|
|
/** 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;
|
|
}
|
|
}
|