Remove reliance on great but complicated mocks
This commit is contained in:
parent
a5731c75ef
commit
eeeb774668
|
@ -13,6 +13,7 @@ module.exports = {
|
||||||
Modal: class { },
|
Modal: class { },
|
||||||
PluginSettingTab: class { },
|
PluginSettingTab: class { },
|
||||||
TFolder: class { },
|
TFolder: class { },
|
||||||
|
App: class { }
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
import { MockAppBuilder } from "./testHelpers/AppBuilder";
|
|
||||||
import MyPlugin from "./main";
|
import MyPlugin from "./main";
|
||||||
import { PluginManifest } from "obsidian";
|
import { App, PluginManifest } from "obsidian";
|
||||||
|
|
||||||
jest.mock("obsidian");
|
jest.mock("obsidian");
|
||||||
(window.setInterval as unknown) = jest.fn();
|
(window.setInterval as unknown) = jest.fn();
|
||||||
|
|
||||||
const app = MockAppBuilder.make();
|
|
||||||
|
|
||||||
describe("MyPlugin", () => {
|
describe("MyPlugin", () => {
|
||||||
let plugin: MyPlugin;
|
let plugin: MyPlugin;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
plugin = new MyPlugin(app.done(), {} as PluginManifest);
|
plugin = new MyPlugin(new App(), {} as PluginManifest);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should register two open modal commands", async () => {
|
it("Should register two open modal commands", async () => {
|
||||||
|
@ -35,7 +32,6 @@ describe("MyPlugin", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should load data and save as settings", async () => {
|
it("Should load data and save as settings", async () => {
|
||||||
const plugin = new MyPlugin(app.done(), {} as PluginManifest);
|
|
||||||
const loadDataSpy = jest.spyOn(plugin, "loadData");
|
const loadDataSpy = jest.spyOn(plugin, "loadData");
|
||||||
await plugin.onload();
|
await plugin.onload();
|
||||||
expect(loadDataSpy).toHaveBeenCalled();
|
expect(loadDataSpy).toHaveBeenCalled();
|
||||||
|
|
|
@ -1,174 +0,0 @@
|
||||||
import {
|
|
||||||
App,
|
|
||||||
CachedMetadata,
|
|
||||||
EventRef,
|
|
||||||
FileManager,
|
|
||||||
Keymap,
|
|
||||||
MetadataCache,
|
|
||||||
Scope,
|
|
||||||
TAbstractFile,
|
|
||||||
TFile,
|
|
||||||
TFolder,
|
|
||||||
UserEvent,
|
|
||||||
Workspace,
|
|
||||||
} from "obsidian";
|
|
||||||
import { join } from "path";
|
|
||||||
import { FileBuilder } from "./FileBuilder";
|
|
||||||
import { MockVault } from "./MockVault";
|
|
||||||
|
|
||||||
export class MockCache implements MetadataCache {
|
|
||||||
private cache: Map<string, CachedMetadata>;
|
|
||||||
|
|
||||||
constructor(cache: Map<string, CachedMetadata>) {
|
|
||||||
this.cache = cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCache(path: string): CachedMetadata | null {
|
|
||||||
return this.cache.get(join("/", path)) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFileCache(file: TFile): CachedMetadata | null {
|
|
||||||
return this.getCache(file.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Below here is not implemented.
|
|
||||||
|
|
||||||
getFirstLinkpathDest(linkpath: string, sourcePath: string): TFile | null {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
fileToLinktext(
|
|
||||||
file: TFile,
|
|
||||||
sourcePath: string,
|
|
||||||
omitMdExtension?: boolean | undefined
|
|
||||||
): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
resolvedLinks: Record<string, Record<string, number>> = {};
|
|
||||||
unresolvedLinks: Record<string, Record<string, number>> = {};
|
|
||||||
on(
|
|
||||||
name: "changed",
|
|
||||||
callback: (file: TFile, data: string, cache: CachedMetadata) => unknown,
|
|
||||||
ctx?: unknown
|
|
||||||
): EventRef;
|
|
||||||
on(
|
|
||||||
name: "deleted",
|
|
||||||
callback: (file: TFile, prevCache: CachedMetadata | null) => unknown,
|
|
||||||
ctx?: unknown
|
|
||||||
): EventRef;
|
|
||||||
on(
|
|
||||||
name: "resolve",
|
|
||||||
callback: (file: TFile) => unknown,
|
|
||||||
ctx?: unknown
|
|
||||||
): EventRef;
|
|
||||||
on(name: "resolved", callback: () => unknown, ctx?: unknown): EventRef;
|
|
||||||
on(
|
|
||||||
name: unknown,
|
|
||||||
callback: unknown,
|
|
||||||
ctx?: unknown
|
|
||||||
): import("obsidian").EventRef {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
off(name: string, callback: (...data: unknown[]) => unknown): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
offref(ref: EventRef): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
trigger(name: string, ...data: unknown[]): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
tryTrigger(evt: EventRef, args: unknown[]): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MockApp implements App {
|
|
||||||
keymap: Keymap = {} as Keymap;
|
|
||||||
scope: Scope = {} as Scope;
|
|
||||||
workspace: Workspace = {} as Workspace;
|
|
||||||
lastEvent: UserEvent | null = null;
|
|
||||||
|
|
||||||
fileManager: FileManager = {} as FileManager;
|
|
||||||
metadataCache: MetadataCache;
|
|
||||||
vault: MockVault;
|
|
||||||
|
|
||||||
constructor(vault: MockVault, cache: MockCache) {
|
|
||||||
this.vault = vault;
|
|
||||||
this.metadataCache = cache;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FileTree<T> {
|
|
||||||
[key: string]: { t: "file"; v: T } | { t: "folder"; v: FileTree<T> };
|
|
||||||
}
|
|
||||||
|
|
||||||
function toPathMap<T>(tree: FileTree<T>): Map<string, T> {
|
|
||||||
const recurse = (t: FileTree<T>, path: string): [string, T][] =>
|
|
||||||
Object.entries(t).flatMap(([name, v]) =>
|
|
||||||
v.t === "file"
|
|
||||||
? [[join(path, name), v.v]]
|
|
||||||
: recurse(v.v, join(path, name))
|
|
||||||
);
|
|
||||||
return new Map(recurse(tree, "/"));
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MockAppBuilder {
|
|
||||||
children: TAbstractFile[];
|
|
||||||
metadata: FileTree<CachedMetadata>;
|
|
||||||
contents: FileTree<string>;
|
|
||||||
path: string;
|
|
||||||
|
|
||||||
static make() {
|
|
||||||
return new MockAppBuilder("/", [], {}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
path: string,
|
|
||||||
children: TAbstractFile[] = [],
|
|
||||||
contents: FileTree<string> = {},
|
|
||||||
metadata: FileTree<CachedMetadata> = {}
|
|
||||||
) {
|
|
||||||
this.path = join("/", path);
|
|
||||||
this.children = children;
|
|
||||||
this.metadata = metadata;
|
|
||||||
this.contents = contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
file(filename: string, builder: FileBuilder): MockAppBuilder {
|
|
||||||
const file = new TFile();
|
|
||||||
file.name = filename;
|
|
||||||
|
|
||||||
const [contents, metadata] = builder.done();
|
|
||||||
|
|
||||||
return new MockAppBuilder(
|
|
||||||
this.path,
|
|
||||||
[...this.children, file],
|
|
||||||
{ ...this.contents, [filename]: { t: "file", v: contents } },
|
|
||||||
{ ...this.metadata, [filename]: { t: "file", v: metadata } }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
folder(f: MockAppBuilder) {
|
|
||||||
return new MockAppBuilder(
|
|
||||||
this.path,
|
|
||||||
[...this.children, f.makeFolder()],
|
|
||||||
{ ...this.contents, [f.path]: { t: "folder", v: f.contents } },
|
|
||||||
{ ...this.metadata, [f.path]: { t: "folder", v: f.metadata } }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private makeFolder(): TFolder {
|
|
||||||
const folder = new TFolder();
|
|
||||||
folder.name = this.path;
|
|
||||||
this.children.forEach((f) => (f.parent = folder));
|
|
||||||
folder.children = [...this.children];
|
|
||||||
return folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
done(): MockApp {
|
|
||||||
return new MockApp(
|
|
||||||
new MockVault(this.makeFolder(), toPathMap(this.contents)),
|
|
||||||
new MockCache(toPathMap(this.metadata))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,247 +0,0 @@
|
||||||
import { CachedMetadata, ListItemCache, Loc, Pos } from "obsidian";
|
|
||||||
|
|
||||||
type ListItem = {
|
|
||||||
type: "item";
|
|
||||||
text: string;
|
|
||||||
checkbox: "x" | " " | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ListBlock = {
|
|
||||||
type: "list";
|
|
||||||
items: (ListItem | ListBlock)[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ListBlockItem = {
|
|
||||||
type: "listitem";
|
|
||||||
text: string;
|
|
||||||
checkbox: " " | "x" | undefined;
|
|
||||||
depth: number;
|
|
||||||
parent: number; // TODO: Higher-level helper to build full lists to properly set the parent.
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeListItems = (
|
|
||||||
list: ListBlock,
|
|
||||||
startingLine: number,
|
|
||||||
depth = 0
|
|
||||||
): ListBlockItem[] => {
|
|
||||||
if (startingLine === 0) {
|
|
||||||
// Special case from Obsidian. My guess is that plugins can check for nested
|
|
||||||
// lists with `parent > 0` rather than `parent >= 0`.
|
|
||||||
startingLine = 1;
|
|
||||||
}
|
|
||||||
const lines: ListBlockItem[] = [];
|
|
||||||
for (const i of list.items) {
|
|
||||||
if (i.type === "item") {
|
|
||||||
lines.push({
|
|
||||||
type: "listitem",
|
|
||||||
text: i.text,
|
|
||||||
checkbox: i.checkbox,
|
|
||||||
depth,
|
|
||||||
parent: depth === 0 ? -startingLine : startingLine,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
lines.push(
|
|
||||||
...makeListItems(i, startingLine + lines.length - 1, depth + 1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lines;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FileBlock =
|
|
||||||
| {
|
|
||||||
type: "heading";
|
|
||||||
text: string;
|
|
||||||
level: number;
|
|
||||||
}
|
|
||||||
| ListBlock
|
|
||||||
| { type: "frontmatter"; key: string; text: string }
|
|
||||||
| { type: "text"; text: string };
|
|
||||||
|
|
||||||
const makeFile = (
|
|
||||||
lines: FileBlock[],
|
|
||||||
tabChars = " ".repeat(4)
|
|
||||||
): [string, CachedMetadata] => {
|
|
||||||
let lineNum = 0;
|
|
||||||
let content = "";
|
|
||||||
|
|
||||||
const appendLine = (line: string): Pos => {
|
|
||||||
const start: Loc = { line: lineNum, col: 0, offset: content.length };
|
|
||||||
content += `${line}\n`;
|
|
||||||
const end: Loc = {
|
|
||||||
line: lineNum++,
|
|
||||||
col: line.length, // Columns are 0-indexed
|
|
||||||
offset: content.length - 1, // Account for the newline and 0-indexing.
|
|
||||||
};
|
|
||||||
return { start, end };
|
|
||||||
};
|
|
||||||
|
|
||||||
const meta: CachedMetadata = {};
|
|
||||||
|
|
||||||
const frontmatter = lines.flatMap((l) =>
|
|
||||||
l.type === "frontmatter" ? l : []
|
|
||||||
);
|
|
||||||
|
|
||||||
if (frontmatter.length > 0) {
|
|
||||||
const data: { [key: string]: unknown } = {};
|
|
||||||
const { start } = appendLine("---");
|
|
||||||
for (const elt of frontmatter) {
|
|
||||||
if (Array.isArray(elt.text)) {
|
|
||||||
appendLine(`${elt.key}: [${elt.text}]`);
|
|
||||||
} else {
|
|
||||||
appendLine(`${elt.key}: ${elt.text}`);
|
|
||||||
}
|
|
||||||
data[elt.key] = elt.text;
|
|
||||||
}
|
|
||||||
const { end } = appendLine("---");
|
|
||||||
meta.frontmatter = {
|
|
||||||
position: { start, end },
|
|
||||||
...data,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const blocks = lines.flatMap((l) => (l.type !== "frontmatter" ? l : []));
|
|
||||||
for (const block of blocks) {
|
|
||||||
switch (block.type) {
|
|
||||||
case "heading": {
|
|
||||||
if (!meta.headings) {
|
|
||||||
meta.headings = [];
|
|
||||||
}
|
|
||||||
const position = appendLine(
|
|
||||||
"#".repeat(block.level) + " " + block.text
|
|
||||||
);
|
|
||||||
meta.headings.push({
|
|
||||||
position,
|
|
||||||
heading: block.text,
|
|
||||||
level: block.level,
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "list": {
|
|
||||||
if (!meta.listItems) {
|
|
||||||
meta.listItems = [];
|
|
||||||
}
|
|
||||||
for (const item of makeListItems(block, lineNum)) {
|
|
||||||
const indent = tabChars.repeat(item.depth);
|
|
||||||
const position = appendLine(
|
|
||||||
indent +
|
|
||||||
"- " +
|
|
||||||
(item.checkbox ? `[${item.checkbox}] ` : "") +
|
|
||||||
item.text
|
|
||||||
);
|
|
||||||
if (indent.length > 0) {
|
|
||||||
position.start.col += indent.length - 2;
|
|
||||||
position.start.offset += indent.length - 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const listItem: ListItemCache = {
|
|
||||||
position,
|
|
||||||
parent: item.parent,
|
|
||||||
};
|
|
||||||
if (item.checkbox) {
|
|
||||||
listItem["task"] = item.checkbox;
|
|
||||||
}
|
|
||||||
|
|
||||||
meta.listItems.push(listItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "text":
|
|
||||||
appendLine(block.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [content, meta];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build up lists that can be consumed by the FileBuilder.
|
|
||||||
*/
|
|
||||||
export class ListBuilder {
|
|
||||||
items: (ListItem | ListBlock)[] = [];
|
|
||||||
|
|
||||||
constructor(items: (ListItem | ListBlock)[] = []) {
|
|
||||||
this.items = items;
|
|
||||||
}
|
|
||||||
|
|
||||||
item(text: string, checkbox?: boolean | undefined) {
|
|
||||||
const i: ListItem = {
|
|
||||||
type: "item",
|
|
||||||
text,
|
|
||||||
checkbox:
|
|
||||||
checkbox === true ? "x" : checkbox === false ? " " : undefined,
|
|
||||||
};
|
|
||||||
return new ListBuilder([...this.items, i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
list(lb: ListBuilder) {
|
|
||||||
return new ListBuilder([...this.items, lb.done()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
done(): ListBlock {
|
|
||||||
return { type: "list", items: this.items };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build up file contents and metadata entries simultaneously. Basically a reverse-parser
|
|
||||||
* for metadata. Construct files line-by-line and metadata that *would* be parsed by
|
|
||||||
* Obsidian is generated at the same time!
|
|
||||||
*/
|
|
||||||
export class FileBuilder {
|
|
||||||
private lines: FileBlock[];
|
|
||||||
|
|
||||||
constructor(lines: FileBlock[] = []) {
|
|
||||||
this.lines = lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add frontmatter to the file.
|
|
||||||
* @param frontmatter Dictionary representing YAML frontmatter.
|
|
||||||
* @returns an updated FileBuilder
|
|
||||||
*/
|
|
||||||
frontmatter(frontmatter: Record<string, string>): FileBuilder {
|
|
||||||
const frontmatterLines = Object.entries(frontmatter).map(
|
|
||||||
([k, v]): FileBlock => ({ type: "frontmatter", key: k, text: v })
|
|
||||||
);
|
|
||||||
return new FileBuilder([...this.lines, ...frontmatterLines]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a heading to the file.
|
|
||||||
* @param level Heading level (h1, h2, etc.)
|
|
||||||
* @param text Text for heading
|
|
||||||
* @returns an updated FileBuilder.
|
|
||||||
*/
|
|
||||||
heading(level: number, text: string): FileBuilder {
|
|
||||||
return new FileBuilder([
|
|
||||||
...this.lines,
|
|
||||||
{ type: "heading", level, text },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a plain text paragraph to the file.
|
|
||||||
* @param text Text body
|
|
||||||
* @returns an updated FileBuilder
|
|
||||||
*/
|
|
||||||
text(text: string): FileBuilder {
|
|
||||||
return new FileBuilder([...this.lines, { type: "text", text }]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a list to the file.
|
|
||||||
* @param list a ListBuilder
|
|
||||||
* @returns an updated FileBuilder
|
|
||||||
*/
|
|
||||||
list(list: ListBuilder): FileBuilder {
|
|
||||||
return new FileBuilder([...this.lines, list.done()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
done() {
|
|
||||||
return makeFile(this.lines);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,265 +0,0 @@
|
||||||
import {
|
|
||||||
DataAdapter,
|
|
||||||
DataWriteOptions,
|
|
||||||
EventRef,
|
|
||||||
TAbstractFile,
|
|
||||||
TFile,
|
|
||||||
TFolder,
|
|
||||||
Vault,
|
|
||||||
} from "obsidian";
|
|
||||||
import { basename, dirname, join, normalize } from "path";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return all files that exist under a given folder.
|
|
||||||
* @param folder Folder to collect children under.
|
|
||||||
* @returns All files under this folder, recursively.
|
|
||||||
*/
|
|
||||||
const collectChildren = (folder: TFolder): TAbstractFile[] => {
|
|
||||||
return folder.children.flatMap((f) => {
|
|
||||||
if (f instanceof TFolder) {
|
|
||||||
return [f, ...collectChildren(f)];
|
|
||||||
} else {
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export class MockVault implements Vault {
|
|
||||||
root: TFolder;
|
|
||||||
contents: Map<string, string>;
|
|
||||||
|
|
||||||
constructor(root: TFolder, contents: Map<string, string>) {
|
|
||||||
this.root = root;
|
|
||||||
this.contents = contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
// These aren't implemented in the mock.
|
|
||||||
adapter: DataAdapter = {} as DataAdapter;
|
|
||||||
configDir = "";
|
|
||||||
|
|
||||||
getName(): string {
|
|
||||||
return "Mock Vault";
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllLoadedFiles(): TAbstractFile[] {
|
|
||||||
return [this.root, ...collectChildren(this.root)];
|
|
||||||
}
|
|
||||||
|
|
||||||
getAbstractFileByPath(path: string): TAbstractFile | null {
|
|
||||||
const normalizedPath = join("/", normalize(path));
|
|
||||||
return (
|
|
||||||
this.getAllLoadedFiles().find(
|
|
||||||
(f) => join("/", normalize(f.path)) === normalizedPath
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
getRoot(): TFolder {
|
|
||||||
return this.root;
|
|
||||||
}
|
|
||||||
async read(file: TFile): Promise<string> {
|
|
||||||
const p = join("/", file.path);
|
|
||||||
const contents = this.contents.get(p);
|
|
||||||
if (!contents) {
|
|
||||||
throw new Error(`File at path ${p} does not have contents`);
|
|
||||||
}
|
|
||||||
return contents;
|
|
||||||
}
|
|
||||||
cachedRead(file: TFile): Promise<string> {
|
|
||||||
return this.read(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
getFiles(): TFile[] {
|
|
||||||
return this.getAllLoadedFiles().flatMap((f) =>
|
|
||||||
f instanceof TFile ? f : []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getMarkdownFiles(): TFile[] {
|
|
||||||
return this.getFiles().filter(
|
|
||||||
(f) => f.extension.toLowerCase() === "md"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setParent(path: string, f: TAbstractFile) {
|
|
||||||
const parentPath = dirname(path);
|
|
||||||
const folder = this.getAbstractFileByPath(parentPath);
|
|
||||||
if (folder instanceof TFolder) {
|
|
||||||
f.parent = folder;
|
|
||||||
folder.children.push(f);
|
|
||||||
}
|
|
||||||
throw new Error("Parent path is not folder.");
|
|
||||||
}
|
|
||||||
|
|
||||||
process(
|
|
||||||
file: TFile,
|
|
||||||
fn: (data: string) => string,
|
|
||||||
options?: DataWriteOptions | undefined
|
|
||||||
): Promise<string> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(
|
|
||||||
path: string,
|
|
||||||
data: string,
|
|
||||||
options?: DataWriteOptions | undefined
|
|
||||||
): Promise<TFile> {
|
|
||||||
if (this.getAbstractFileByPath(path)) {
|
|
||||||
throw new Error("File already exists.");
|
|
||||||
}
|
|
||||||
const file = new TFile();
|
|
||||||
file.name = basename(path);
|
|
||||||
this.setParent(path, file);
|
|
||||||
this.contents.set(path, data);
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
async createFolder(path: string): Promise<TFolder> {
|
|
||||||
const folder = new TFolder();
|
|
||||||
folder.name = basename(path);
|
|
||||||
this.setParent(path, folder);
|
|
||||||
return folder;
|
|
||||||
}
|
|
||||||
async delete(
|
|
||||||
file: TAbstractFile,
|
|
||||||
force?: boolean | undefined
|
|
||||||
): Promise<void> {
|
|
||||||
file.parent?.children.remove(file);
|
|
||||||
}
|
|
||||||
trash(file: TAbstractFile, system: boolean): Promise<void> {
|
|
||||||
return this.delete(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
async rename(file: TAbstractFile, newPath: string): Promise<void> {
|
|
||||||
const newParentPath = dirname(newPath);
|
|
||||||
const newParent = this.getAbstractFileByPath(newParentPath);
|
|
||||||
if (!(newParent instanceof TFolder)) {
|
|
||||||
throw new Error(`No such folder: ${newParentPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file instanceof TFile) {
|
|
||||||
// If we're renaming a file, just update the parent and name in the
|
|
||||||
// file, and the entry in the content map.
|
|
||||||
const contents = this.contents.get(file.path);
|
|
||||||
if (!contents) {
|
|
||||||
throw new Error(`File did not have contents: ${file.path}`);
|
|
||||||
}
|
|
||||||
this.contents.delete(file.path);
|
|
||||||
|
|
||||||
// Update the parent and name and re-set contents with the new path.
|
|
||||||
// NOTE: This relies on using the included mock that derives the path
|
|
||||||
// from the parent and filename as a getter property.
|
|
||||||
file.parent = newParent;
|
|
||||||
file.name = basename(newPath);
|
|
||||||
this.contents.set(file.path, contents);
|
|
||||||
} else if (file instanceof TFolder) {
|
|
||||||
// If we're renaming a folder, we need to update the content map for
|
|
||||||
// every TFile under this folder.
|
|
||||||
|
|
||||||
// Collect all files under this folder, get the string contents, delete
|
|
||||||
// the entry for the old path, and return the file and contents in a tuple.
|
|
||||||
const filesAndContents = collectChildren(file)
|
|
||||||
.flatMap((f) => (f instanceof TFile ? f : []))
|
|
||||||
.map((f): [TFile, string] => {
|
|
||||||
const contents = this.contents.get(f.path);
|
|
||||||
if (!contents) {
|
|
||||||
throw new Error(
|
|
||||||
`File did not have contents: ${f.path}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.contents.delete(f.path);
|
|
||||||
return [f, contents];
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the parent and name for this folder.
|
|
||||||
file.parent = newParent;
|
|
||||||
file.name = basename(newPath);
|
|
||||||
|
|
||||||
// Re-add all the paths to the content dir.
|
|
||||||
for (const [f, contents] of filesAndContents) {
|
|
||||||
this.contents.set(f.path, contents);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`File is not a file or folder: ${file.path}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async modify(
|
|
||||||
file: TFile,
|
|
||||||
data: string,
|
|
||||||
options?: DataWriteOptions | undefined
|
|
||||||
): Promise<void> {
|
|
||||||
this.contents.set(file.path, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async copy(file: TFile, newPath: string): Promise<TFile> {
|
|
||||||
const data = await this.read(file);
|
|
||||||
return await this.create(newPath, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement callbacks.
|
|
||||||
on(
|
|
||||||
name: "create",
|
|
||||||
callback: (file: TAbstractFile) => unknown,
|
|
||||||
ctx?: unknown
|
|
||||||
): EventRef;
|
|
||||||
on(
|
|
||||||
name: "modify",
|
|
||||||
callback: (file: TAbstractFile) => unknown,
|
|
||||||
ctx?: unknown
|
|
||||||
): EventRef;
|
|
||||||
on(
|
|
||||||
name: "delete",
|
|
||||||
callback: (file: TAbstractFile) => unknown,
|
|
||||||
ctx?: unknown
|
|
||||||
): EventRef;
|
|
||||||
on(
|
|
||||||
name: "rename",
|
|
||||||
callback: (file: TAbstractFile, oldPath: string) => unknown,
|
|
||||||
ctx?: unknown
|
|
||||||
): EventRef;
|
|
||||||
on(name: "closed", callback: () => unknown, ctx?: unknown): EventRef;
|
|
||||||
on(name: unknown, callback: unknown, ctx?: unknown): EventRef {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
off(name: string, callback: (...data: unknown[]) => unknown): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
offref(ref: EventRef): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
trigger(name: string, ...data: unknown[]): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
tryTrigger(evt: EventRef, args: unknown[]): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
append(
|
|
||||||
file: TFile,
|
|
||||||
data: string,
|
|
||||||
options?: DataWriteOptions | undefined
|
|
||||||
): Promise<void> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
createBinary(
|
|
||||||
path: string,
|
|
||||||
data: ArrayBuffer,
|
|
||||||
options?: DataWriteOptions | undefined
|
|
||||||
): Promise<TFile> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
readBinary(file: TFile): Promise<ArrayBuffer> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
modifyBinary(
|
|
||||||
file: TFile,
|
|
||||||
data: ArrayBuffer,
|
|
||||||
options?: DataWriteOptions | undefined
|
|
||||||
): Promise<void> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
getResourcePath(file: TFile): string {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue