Begin adding tests
This commit is contained in:
parent
e089616deb
commit
bc5ddf3c23
1
.npmignore
Normal file
1
.npmignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
bin/_test
|
|
@ -9,7 +9,7 @@
|
||||||
"build": "npm run clean && tsc",
|
"build": "npm run clean && tsc",
|
||||||
"clean": "shx rm -rf bin",
|
"clean": "shx rm -rf bin",
|
||||||
"dev": "npm run build && npm start",
|
"dev": "npm run build && npm start",
|
||||||
"start": "node ./bin/hammerstone.js"
|
"test": "npm run build && shx cp -r src/_test/test-vault bin/_test/test-vault && node ./bin/_test/test-runner.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|
29
src/_test/main.test.ts
Normal file
29
src/_test/main.test.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import test, { describe } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import Vault from "../vault.js";
|
||||||
|
|
||||||
|
describe("Vault", () => {
|
||||||
|
test("load a vault", () => {
|
||||||
|
const vault = new Vault("./bin/_test/test-vault");
|
||||||
|
assert.equal(vault.size, 4, "Unexpected number of documents in vault");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ignore paths", () => {
|
||||||
|
const vault = new Vault("./bin/_test/test-vault", {
|
||||||
|
ignorePatterns: ["**/Ignoreme/**"],
|
||||||
|
});
|
||||||
|
assert.equal(vault.size, 3, "Unexpected number of documents in vault");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reading tags", () => {
|
||||||
|
const vault = new Vault("./bin/_test/test-vault");
|
||||||
|
const taggedFile = vault.index["directory-subdirectory-i-have-tags"];
|
||||||
|
|
||||||
|
if (taggedFile === undefined) {
|
||||||
|
assert.fail("Expected file with tags");
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedTags = ["propertytag", "tags", "hashtags"];
|
||||||
|
assert.deepStrictEqual(taggedFile.tags, expectedTags);
|
||||||
|
});
|
||||||
|
});
|
13
src/_test/test-runner.ts
Normal file
13
src/_test/test-runner.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { tap } from "node:test/reporters";
|
||||||
|
import { run } from "node:test";
|
||||||
|
import process from "node:process";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
run({
|
||||||
|
files: [path.resolve("./bin/_test/main.test.js")],
|
||||||
|
})
|
||||||
|
.on("test:fail", () => {
|
||||||
|
process.exitCode = 1;
|
||||||
|
})
|
||||||
|
.compose(tap)
|
||||||
|
.pipe(process.stdout);
|
1
src/_test/test-vault/.obsidian/app.json
vendored
Normal file
1
src/_test/test-vault/.obsidian/app.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
3
src/_test/test-vault/.obsidian/appearance.json
vendored
Normal file
3
src/_test/test-vault/.obsidian/appearance.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"accentColor": ""
|
||||||
|
}
|
30
src/_test/test-vault/.obsidian/core-plugins-migration.json
vendored
Normal file
30
src/_test/test-vault/.obsidian/core-plugins-migration.json
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"file-explorer": true,
|
||||||
|
"global-search": true,
|
||||||
|
"switcher": true,
|
||||||
|
"graph": true,
|
||||||
|
"backlink": true,
|
||||||
|
"canvas": true,
|
||||||
|
"outgoing-link": true,
|
||||||
|
"tag-pane": true,
|
||||||
|
"properties": false,
|
||||||
|
"page-preview": true,
|
||||||
|
"daily-notes": true,
|
||||||
|
"templates": true,
|
||||||
|
"note-composer": true,
|
||||||
|
"command-palette": true,
|
||||||
|
"slash-command": false,
|
||||||
|
"editor-status": true,
|
||||||
|
"bookmarks": true,
|
||||||
|
"markdown-importer": false,
|
||||||
|
"zk-prefixer": false,
|
||||||
|
"random-note": false,
|
||||||
|
"outline": true,
|
||||||
|
"word-count": true,
|
||||||
|
"slides": false,
|
||||||
|
"audio-recorder": false,
|
||||||
|
"workspaces": false,
|
||||||
|
"file-recovery": true,
|
||||||
|
"publish": false,
|
||||||
|
"sync": false
|
||||||
|
}
|
20
src/_test/test-vault/.obsidian/core-plugins.json
vendored
Normal file
20
src/_test/test-vault/.obsidian/core-plugins.json
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
[
|
||||||
|
"file-explorer",
|
||||||
|
"global-search",
|
||||||
|
"switcher",
|
||||||
|
"graph",
|
||||||
|
"backlink",
|
||||||
|
"canvas",
|
||||||
|
"outgoing-link",
|
||||||
|
"tag-pane",
|
||||||
|
"page-preview",
|
||||||
|
"daily-notes",
|
||||||
|
"templates",
|
||||||
|
"note-composer",
|
||||||
|
"command-palette",
|
||||||
|
"editor-status",
|
||||||
|
"bookmarks",
|
||||||
|
"outline",
|
||||||
|
"word-count",
|
||||||
|
"file-recovery"
|
||||||
|
]
|
22
src/_test/test-vault/.obsidian/graph.json
vendored
Normal file
22
src/_test/test-vault/.obsidian/graph.json
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"collapse-filter": true,
|
||||||
|
"search": "",
|
||||||
|
"showTags": false,
|
||||||
|
"showAttachments": false,
|
||||||
|
"hideUnresolved": false,
|
||||||
|
"showOrphans": true,
|
||||||
|
"collapse-color-groups": true,
|
||||||
|
"colorGroups": [],
|
||||||
|
"collapse-display": true,
|
||||||
|
"showArrow": false,
|
||||||
|
"textFadeMultiplier": 0,
|
||||||
|
"nodeSizeMultiplier": 1,
|
||||||
|
"lineSizeMultiplier": 1,
|
||||||
|
"collapse-forces": true,
|
||||||
|
"centerStrength": 0.518713248970312,
|
||||||
|
"repelStrength": 10,
|
||||||
|
"linkStrength": 1,
|
||||||
|
"linkDistance": 250,
|
||||||
|
"scale": 1,
|
||||||
|
"close": true
|
||||||
|
}
|
160
src/_test/test-vault/.obsidian/workspace.json
vendored
Normal file
160
src/_test/test-vault/.obsidian/workspace.json
vendored
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
{
|
||||||
|
"main": {
|
||||||
|
"id": "0d432b44da4e24cf",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "6bba5a91ffd45e55",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "c548f27295fc9f89",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "markdown",
|
||||||
|
"state": {
|
||||||
|
"file": "Ignoreme/Unimportant file.md",
|
||||||
|
"mode": "source",
|
||||||
|
"source": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "vertical"
|
||||||
|
},
|
||||||
|
"left": {
|
||||||
|
"id": "eb8f617338bff33a",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "e9168ba4d39b9811",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "0714e101e8d2e95b",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "file-explorer",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "alphabetical"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d8fde6378b886f78",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "search",
|
||||||
|
"state": {
|
||||||
|
"query": "",
|
||||||
|
"matchingCase": false,
|
||||||
|
"explainSearch": false,
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2f84c2d6a1864258",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "bookmarks",
|
||||||
|
"state": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"id": "5dc6da21227cd234",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "7ce4d629d6f2274c",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "996e1a5e0748190e",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "backlink",
|
||||||
|
"state": {
|
||||||
|
"file": "Ignoreme/Unimportant file.md",
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": "",
|
||||||
|
"backlinkCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "aaed9afdd72608ad",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outgoing-link",
|
||||||
|
"state": {
|
||||||
|
"file": "Ignoreme/Unimportant file.md",
|
||||||
|
"linksCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "71cf6badf7dcaf51",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "tag",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"useHierarchy": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "93936f2b9e8b5004",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outline",
|
||||||
|
"state": {
|
||||||
|
"file": "Ignoreme/Unimportant file.md"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300,
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
"left-ribbon": {
|
||||||
|
"hiddenItems": {
|
||||||
|
"switcher:Open quick switcher": false,
|
||||||
|
"graph:Open graph view": false,
|
||||||
|
"canvas:Create new canvas": false,
|
||||||
|
"daily-notes:Open today's daily note": false,
|
||||||
|
"templates:Insert template": false,
|
||||||
|
"command-palette:Open command palette": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": "c548f27295fc9f89",
|
||||||
|
"lastOpenFiles": [
|
||||||
|
"Another Directory/Cool note.md",
|
||||||
|
"Ignoreme/Unimportant file.md",
|
||||||
|
"Ignoreme",
|
||||||
|
"Directory/Subdirectory/I have tags.md",
|
||||||
|
"Another Directory",
|
||||||
|
"Welcome.md",
|
||||||
|
"Directory/Subdirectory",
|
||||||
|
"Directory"
|
||||||
|
]
|
||||||
|
}
|
1
src/_test/test-vault/Another Directory/Cool note.md
Normal file
1
src/_test/test-vault/Another Directory/Cool note.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This is a cool cool note, it also links to the [[Welcome]] note.
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
tags:
|
||||||
|
- propertytag
|
||||||
|
---
|
||||||
|
I am a file that has some #tags in it, using inline #hashtags as well as the properties.
|
1
src/_test/test-vault/Ignoreme/Unimportant file.md
Normal file
1
src/_test/test-vault/Ignoreme/Unimportant file.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This file should not be read because it is not important.
|
1
src/_test/test-vault/Welcome.md
Normal file
1
src/_test/test-vault/Welcome.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This is the home document of this here test vault.
|
165
src/document.ts
165
src/document.ts
|
@ -1,116 +1,14 @@
|
||||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import slug from "slug";
|
import slug from "slug";
|
||||||
import YAML from "yaml";
|
|
||||||
import type Vault from "./vault.js";
|
import type Vault from "./vault.js";
|
||||||
|
import {
|
||||||
export type FrontmatterShape = Record<string, string | string[]>;
|
combineMarkdownAndFrontmatter,
|
||||||
|
extractFrontmatter,
|
||||||
const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm;
|
extractMarkdown,
|
||||||
|
extractTags,
|
||||||
function getBlockTags(blockName: string): [string, string] {
|
type FrontmatterShape,
|
||||||
return [`%% {${blockName}} %%`, `%% {/${blockName}} %%`];
|
} from "./markdown.js";
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
export default class MarkdownDocument {
|
||||||
/** A reference to the vault containing this document */
|
/** A reference to the vault containing this document */
|
||||||
|
@ -119,12 +17,10 @@ export default class MarkdownDocument {
|
||||||
/** The original file path to this document */
|
/** The original file path to this document */
|
||||||
readonly path: string;
|
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 */
|
/** The directory/folder that this Document is in, relative to the vault root */
|
||||||
readonly dirname: string;
|
readonly dirname: string;
|
||||||
|
|
||||||
|
/** An array of directory names this document is found under */
|
||||||
readonly taxonomy: string[];
|
readonly taxonomy: string[];
|
||||||
|
|
||||||
/** The name of this document's file, without the file extension */
|
/** The name of this document's file, without the file extension */
|
||||||
|
@ -148,6 +44,8 @@ export default class MarkdownDocument {
|
||||||
/** The markdown portion of the file (without frontmatter) */
|
/** The markdown portion of the file (without frontmatter) */
|
||||||
private _markdown: string;
|
private _markdown: string;
|
||||||
|
|
||||||
|
tags: string[];
|
||||||
|
|
||||||
constructor(filePath: string, vault: Vault) {
|
constructor(filePath: string, vault: Vault) {
|
||||||
if (!existsSync(filePath)) {
|
if (!existsSync(filePath)) {
|
||||||
throw new Error(`File not found: ${filePath}`);
|
throw new Error(`File not found: ${filePath}`);
|
||||||
|
@ -156,30 +54,23 @@ export default class MarkdownDocument {
|
||||||
const rawFileContent = readFileSync(filePath, "utf-8");
|
const rawFileContent = readFileSync(filePath, "utf-8");
|
||||||
const fileDirname = path.dirname(filePath);
|
const fileDirname = path.dirname(filePath);
|
||||||
const vaultPath = vault.vaultPath;
|
const vaultPath = vault.vaultPath;
|
||||||
|
const frontmatter = extractFrontmatter(rawFileContent);
|
||||||
|
|
||||||
this.path = filePath;
|
this.path = filePath;
|
||||||
this.dirname = path.relative(vaultPath, fileDirname);
|
this.dirname = path.relative(vaultPath, fileDirname);
|
||||||
this.taxonomy = this.dirname.split(path.sep);
|
this.taxonomy = this.dirname.split(path.sep);
|
||||||
this.vaultRootPath = vaultPath;
|
|
||||||
this.filename = path.basename(filePath, ".md");
|
this.filename = path.basename(filePath, ".md");
|
||||||
this.slug = slug(`${this.taxonomy.join("-")}-${this.filename}`);
|
this.slug = slug(`${this.taxonomy.join("-")}-${this.filename}`);
|
||||||
this._content = rawFileContent;
|
this._content = rawFileContent;
|
||||||
this.contentHistory = [rawFileContent];
|
this.contentHistory = [rawFileContent];
|
||||||
this._frontmatter = processDocumentFrontmatter(rawFileContent);
|
this._frontmatter = frontmatter;
|
||||||
this._markdown = getDocumentMarkdown(rawFileContent);
|
this._markdown = extractMarkdown(rawFileContent);
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
}
|
|
||||||
|
|
||||||
containsBlock(blockName: string): boolean {
|
// Identify tags from content and frontmatter
|
||||||
return documentContainsBlock(this, blockName);
|
const frontmatterTags = frontmatter.tags;
|
||||||
}
|
const contentTags = extractTags(this._markdown);
|
||||||
|
this.tags = frontmatterTags.concat(contentTags);
|
||||||
readBlock(blockName: string): string | undefined {
|
|
||||||
return readDocumentBlock(this, blockName);
|
|
||||||
}
|
|
||||||
|
|
||||||
setBlockContent(blockName: string, newContent: string): MarkdownDocument {
|
|
||||||
return writeDocumentBlock(this, blockName, newContent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
@ -226,11 +117,29 @@ export default class MarkdownDocument {
|
||||||
|
|
||||||
set content(newValue: string) {
|
set content(newValue: string) {
|
||||||
this._content = newValue;
|
this._content = newValue;
|
||||||
this._frontmatter = processDocumentFrontmatter(newValue);
|
this._frontmatter = extractFrontmatter(newValue);
|
||||||
this._markdown = getDocumentMarkdown(newValue);
|
this._markdown = extractMarkdown(newValue);
|
||||||
this.contentHistory.push(this._content);
|
this.contentHistory.push(this._content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasTag(tag: string) {
|
||||||
|
return this.tags.includes(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
write() {
|
write() {
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
this.path,
|
this.path,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Vault from "./vault.js";
|
import Vault from "./vault.js";
|
||||||
import MarkdownDocument, { type FrontmatterShape } from "./document.js";
|
import MarkdownDocument from "./document.js";
|
||||||
|
import type { FrontmatterShape } from "./markdown.js";
|
||||||
|
|
||||||
export { Vault, MarkdownDocument, FrontmatterShape };
|
export { Vault, MarkdownDocument, FrontmatterShape };
|
||||||
|
|
74
src/markdown.ts
Normal file
74
src/markdown.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import YAML from "yaml";
|
||||||
|
|
||||||
|
export interface FrontmatterShape {
|
||||||
|
tags: string[];
|
||||||
|
aliases: string[];
|
||||||
|
cssclasses: string[];
|
||||||
|
[key: string]: string | string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_FRONTMATTER: FrontmatterShape = {
|
||||||
|
tags: [],
|
||||||
|
aliases: [],
|
||||||
|
cssclasses: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to parse YAML frontmatter from a mixed yaml/md doc
|
||||||
|
* @param content The raw markdown content
|
||||||
|
* @returns Any successfully parsed frontmatter
|
||||||
|
*/
|
||||||
|
export function extractFrontmatter(content: string): FrontmatterShape {
|
||||||
|
// If it does not start with `---`, it is invalid for frontmatter
|
||||||
|
if (content.trim().indexOf("---") !== 0) {
|
||||||
|
return {
|
||||||
|
...EMPTY_FRONTMATTER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let frontmatterData = {};
|
||||||
|
|
||||||
|
if (FRONTMATTER_REGEX.test(content)) {
|
||||||
|
const frontmatterString = content.match(FRONTMATTER_REGEX)![0];
|
||||||
|
const cleanFrontmatter = frontmatterString.replaceAll("---", "").trim();
|
||||||
|
frontmatterData = YAML.parse(cleanFrontmatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...EMPTY_FRONTMATTER,
|
||||||
|
...frontmatterData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractMarkdown(content: string): string {
|
||||||
|
if (content.trim().indexOf("---") !== 0) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return content.replace(FRONTMATTER_REGEX, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function combineMarkdownAndFrontmatter(
|
||||||
|
markdown: string,
|
||||||
|
frontmatter: FrontmatterShape,
|
||||||
|
) {
|
||||||
|
return `---
|
||||||
|
${YAML.stringify(frontmatter)}
|
||||||
|
---
|
||||||
|
|
||||||
|
${markdown.trim()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TAG_REGEX = /#[A-Za-z0-9/\-_]+/g;
|
||||||
|
|
||||||
|
export function extractTags(content: string): string[] {
|
||||||
|
const results = content.match(TAG_REGEX);
|
||||||
|
|
||||||
|
if (results === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.map((result) => result.replace("#", ""));
|
||||||
|
}
|
47
src/vault.ts
47
src/vault.ts
|
@ -1,3 +1,4 @@
|
||||||
|
import path from "node:path";
|
||||||
import { globSync } from "glob";
|
import { globSync } from "glob";
|
||||||
import MarkdownDocument from "./document.js";
|
import MarkdownDocument from "./document.js";
|
||||||
|
|
||||||
|
@ -47,22 +48,61 @@ function buildVaultIndex(
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export 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(fn: (document: MarkdownDocument) => MarkdownDocument) {
|
||||||
|
this.documents = this.documents.map(fn);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface VaultOptions {
|
interface VaultOptions {
|
||||||
ignorePatterns?: string[];
|
ignorePatterns?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Vault {
|
export default class Vault {
|
||||||
|
/** An array of all discovered Markdown documents */
|
||||||
documents: MarkdownDocument[] = [];
|
documents: MarkdownDocument[] = [];
|
||||||
|
|
||||||
|
/** An array of all generated document slugs */
|
||||||
slugs: string[] = [];
|
slugs: string[] = [];
|
||||||
|
|
||||||
|
/** A map of generated document slugs to their associated document */
|
||||||
index: Record<string, MarkdownDocument> = {};
|
index: Record<string, MarkdownDocument> = {};
|
||||||
|
|
||||||
|
/** The absolute path of this fault on disk */
|
||||||
readonly vaultPath: string;
|
readonly vaultPath: string;
|
||||||
|
|
||||||
|
/** The number of documents in this vault */
|
||||||
readonly size: number = 0;
|
readonly size: number = 0;
|
||||||
|
|
||||||
|
/** File patterns to ignore when discovering vault files */
|
||||||
private ignorePatterns: string[] = [];
|
private ignorePatterns: string[] = [];
|
||||||
|
|
||||||
constructor(vaultRootPath: string, options?: VaultOptions) {
|
constructor(vaultRootPath: string, options?: VaultOptions) {
|
||||||
this.vaultPath = vaultRootPath;
|
this.vaultPath = path.resolve(vaultRootPath);
|
||||||
this.ignorePatterns = options?.ignorePatterns ?? [];
|
this.ignorePatterns = options?.ignorePatterns ?? [];
|
||||||
|
|
||||||
const allFiles = loadVaultDocuments(this, this.ignorePatterns);
|
const allFiles = loadVaultDocuments(this, this.ignorePatterns);
|
||||||
|
@ -74,6 +114,11 @@ export default class Vault {
|
||||||
this.index = buildVaultIndex(allFiles);
|
this.index = buildVaultIndex(allFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
view(fn: (document: MarkdownDocument) => boolean) {
|
||||||
|
const matchingDocs = this.documents.filter(fn);
|
||||||
|
return new VaultView(matchingDocs, this);
|
||||||
|
}
|
||||||
|
|
||||||
map(fn: (document: MarkdownDocument) => MarkdownDocument) {
|
map(fn: (document: MarkdownDocument) => MarkdownDocument) {
|
||||||
this.documents = this.documents.map(fn);
|
this.documents = this.documents.map(fn);
|
||||||
return this;
|
return this;
|
||||||
|
|
Loading…
Reference in a new issue