224 lines
5.8 KiB
TypeScript
224 lines
5.8 KiB
TypeScript
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
import path from "node:path";
|
|
import slug from "slug";
|
|
import YAML from "yaml";
|
|
|
|
export type FrontmatterShape = Record<string, string | string[]>;
|
|
|
|
const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm;
|
|
|
|
function getBlockTags(blockName: string): [string, string] {
|
|
return [`%% {${blockName}} %%`, `%% {/${blockName}} %%`];
|
|
}
|
|
|
|
function documentContainsBlock(document: MarkdownDocument, blockName: string) {
|
|
const [openTag, closeTag] = getBlockTags(blockName);
|
|
|
|
const openLoc = document.content.indexOf(openTag);
|
|
const closeLoc = document.content.indexOf(closeTag);
|
|
|
|
if (openLoc === -1 || closeLoc === -1) {
|
|
return false;
|
|
}
|
|
|
|
if (closeLoc < openLoc) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function readDocumentBlock(
|
|
document: MarkdownDocument,
|
|
blockName: string,
|
|
): string | undefined {
|
|
const [openTag, closeTag] = getBlockTags(blockName);
|
|
|
|
const openLoc = document.content.indexOf(openTag);
|
|
const closeLoc = document.content.indexOf(closeTag);
|
|
|
|
if (openLoc === -1 || closeLoc === -1) {
|
|
return undefined;
|
|
}
|
|
|
|
return document.content.slice(openLoc + openTag.length, closeLoc).trim();
|
|
}
|
|
|
|
function writeDocumentBlock(
|
|
document: MarkdownDocument,
|
|
blockName: string,
|
|
blockContent: string = "",
|
|
): MarkdownDocument {
|
|
const [openTag, closeTag] = getBlockTags(blockName);
|
|
|
|
const openLoc = document.content.indexOf(openTag);
|
|
const closeLoc = document.content.indexOf(closeTag);
|
|
|
|
if (openLoc === -1 || closeLoc === -1) {
|
|
return document;
|
|
}
|
|
|
|
const newContent =
|
|
document.content.slice(0, openLoc + openTag.length) +
|
|
"\n" +
|
|
blockContent +
|
|
"\n" +
|
|
document.content.slice(closeLoc);
|
|
|
|
document.content = newContent;
|
|
return document;
|
|
}
|
|
|
|
/**
|
|
* Attempt to parse YAML frontmatter from a markdown document
|
|
* @param contents The raw markdown content
|
|
* @returns Any successfully parsed frontmatter
|
|
*/
|
|
function processDocumentFrontmatter(contents: string): FrontmatterShape {
|
|
if (contents.trim().indexOf("---") !== 0) {
|
|
return {};
|
|
}
|
|
|
|
let frontmatterData = {};
|
|
|
|
if (FRONTMATTER_REGEX.test(contents)) {
|
|
const frontmatterString = contents.match(FRONTMATTER_REGEX)![0];
|
|
const cleanFrontmatter = frontmatterString.replaceAll("---", "").trim();
|
|
frontmatterData = YAML.parse(cleanFrontmatter);
|
|
}
|
|
|
|
return {
|
|
...frontmatterData,
|
|
};
|
|
}
|
|
|
|
function getDocumentMarkdown(content: string): string {
|
|
if (content.trim().indexOf("---") !== 0) {
|
|
return content;
|
|
}
|
|
return content.replace(FRONTMATTER_REGEX, "").trim();
|
|
}
|
|
|
|
function combineMarkdownAndFrontmatter(
|
|
markdown: string,
|
|
frontmatter: FrontmatterShape,
|
|
) {
|
|
return `---
|
|
${YAML.stringify(frontmatter)}
|
|
---
|
|
|
|
${markdown.trim()}
|
|
`;
|
|
}
|
|
|
|
export default class MarkdownDocument {
|
|
/** The original file path to this document */
|
|
readonly path: string;
|
|
|
|
/** The path to the root of the vault */
|
|
readonly vaultRootPath: string;
|
|
|
|
/** The directory/folder that this Document is in, relative to the vault root */
|
|
readonly dirname: string;
|
|
|
|
readonly taxonomy: string[];
|
|
|
|
/** The name of this document's file, without the file extension */
|
|
readonly filename: 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, vaultPath: string) {
|
|
if (!existsSync(filePath)) {
|
|
throw new Error(`File not found: ${filePath}`);
|
|
}
|
|
|
|
const rawFileContent = readFileSync(filePath, "utf-8");
|
|
const fileDirname = path.dirname(filePath);
|
|
|
|
this.path = filePath;
|
|
this.dirname = path.relative(vaultPath, fileDirname);
|
|
this.taxonomy = this.dirname.split(path.sep);
|
|
this.vaultRootPath = vaultPath;
|
|
this.filename = path.basename(filePath, ".md");
|
|
this.slug = slug(`${this.taxonomy.join("-")}-${this.filename}`);
|
|
this._content = rawFileContent;
|
|
this.contentHistory = [rawFileContent];
|
|
this._frontmatter = processDocumentFrontmatter(rawFileContent);
|
|
this._markdown = getDocumentMarkdown(rawFileContent);
|
|
}
|
|
|
|
containsBlock(blockName: string): boolean {
|
|
return documentContainsBlock(this, blockName);
|
|
}
|
|
|
|
readBlock(blockName: string): string | undefined {
|
|
return readDocumentBlock(this, blockName);
|
|
}
|
|
|
|
setBlockContent(blockName: string, newContent: string): MarkdownDocument {
|
|
return writeDocumentBlock(this, blockName, newContent);
|
|
}
|
|
|
|
set markdown(newValue: string) {
|
|
this._markdown = newValue;
|
|
this._content = combineMarkdownAndFrontmatter(newValue, this.frontmatter);
|
|
this.contentHistory.push(this._content);
|
|
}
|
|
|
|
/** The markdown portion of the file (without frontmatter) */
|
|
get markdown() {
|
|
return this._markdown;
|
|
}
|
|
|
|
set frontmatter(newValue: FrontmatterShape) {
|
|
this._frontmatter = newValue;
|
|
this._content = combineMarkdownAndFrontmatter(
|
|
this._markdown,
|
|
this._frontmatter,
|
|
);
|
|
this.contentHistory.push(this._content);
|
|
}
|
|
|
|
/** The parsed frontmatter of the file */
|
|
get frontmatter() {
|
|
return this._frontmatter;
|
|
}
|
|
|
|
/** The raw content of the file */
|
|
get content() {
|
|
return this._content;
|
|
}
|
|
|
|
set content(newValue: string) {
|
|
this._content = newValue;
|
|
this._frontmatter = processDocumentFrontmatter(newValue);
|
|
this._markdown = getDocumentMarkdown(newValue);
|
|
this.contentHistory.push(this._content);
|
|
}
|
|
|
|
write() {
|
|
writeFileSync(
|
|
this.path,
|
|
combineMarkdownAndFrontmatter(this._markdown, this.frontmatter),
|
|
"utf-8",
|
|
);
|
|
}
|
|
}
|