import assertNever from "assert-never"; import { Modal, Notice, OpenViewState, TFile, TFolder } from "obsidian"; import { basename as getBase, join } from "path"; import FNCore from "../fnc-main"; import log from "../logger"; import { getParentPath, isMd } from "../misc"; import API, { FolderNotePath, NoteLoc } from "../typings/api"; export default class NoteResolver { plugin: FNCore; constructor(plugin: FNCore) { this.plugin = plugin; } private get settings() { return this.plugin.settings; } private get vault() { return this.plugin.app.vault; } getFolderFromNote: API["getFolderFromNote"] = (note, strategy) => { if (!isMd(note)) return null; const folderPath = this.getFolderPath(note, false, strategy); if (!folderPath) return null; const folder = this.vault.getAbstractFileByPath(folderPath); if (folder && folder instanceof TFolder) return folder; else return null; }; /** * Get path of given note/notePath's folder based on setting * @param note notePath or note TFile * @param newFolder if true, return folder in the same folder as note * @returns folder path, will return null if note basename invaild and newFolder=false */ getFolderPath: API["getFolderPath"] = ( note, newFolder = false, strategy?: NoteLoc, ) => { if (strategy === undefined) strategy = this.settings.folderNotePref; if (!isMd(note)) { log.info("getFolderPath(%o): given file not markdown", note); return null; } let parent: string, base: string; if (note instanceof TFile) { base = note.basename; parent = getParentPath(note.path) ?? ""; } else { base = getBase(note).slice(0, -3); // remove ending ".md" parent = getParentPath(note) ?? ""; } if (!parent) { log.info("getFolderPath(%o): no folder note for root dir", note); return null; } const getSiblingFolder = () => { if (parent === "/") return base; else return join(parent, base); }; switch (strategy) { case NoteLoc.Index: if (newFolder) return getSiblingFolder(); else if (base === this.settings.indexName) return parent; else { log.info("getFolderPath(%o): note name invaild", note); return null; } case NoteLoc.Inside: if (newFolder) return getSiblingFolder(); else if (base === getBase(parent)) return parent; else { log.info("getFolderPath(%o): note name invaild", note); return null; } case NoteLoc.Outside: { const dir = getSiblingFolder(); if (newFolder || base === getBase(dir)) return dir; else { log.info("getFolderPath(%o): note name invaild", note); return null; } } default: assertNever(strategy); } }; // Get Folder Note from Folder getFolderNote: API["getFolderNote"] = (folder, strategy) => this.findFolderNote(this.getFolderNotePath(folder, strategy)); findFolderNote = (info: FolderNotePath | null): TFile | null => { if (!info) return null; const note = this.vault.getAbstractFileByPath(info.path); if (note && note instanceof TFile) return note; else return null; }; getFolderNotePath: API["getFolderNotePath"] = (folder, strategy) => { if (strategy === undefined) strategy = this.settings.folderNotePref; const dirPath = typeof folder === "string" ? folder : folder.path, parent = getParentPath(dirPath); if (!parent) { // is root folder return null; } const { indexName } = this.settings; let dir: string, basename: string; switch (strategy) { case NoteLoc.Index: basename = indexName; dir = dirPath; break; case NoteLoc.Inside: basename = getBase(dirPath); dir = dirPath; break; case NoteLoc.Outside: basename = getBase(dirPath); dir = parent; break; default: assertNever(strategy); } return { dir, name: basename + ".md", path: dir === "/" ? basename + ".md" : join(dir, basename + ".md"), }; }; // Note Operations /** * @returns return false if no linked folder found */ DeleteLinkedFolder: API["DeleteLinkedFolder"] = ( file: TFile, dryrun = false, ): boolean => { if (!isMd(file)) return false; const folderResult = this.getFolderFromNote(file); if (folderResult && !dryrun) this.vault.delete(folderResult, true); return !!folderResult; }; /** * @returns return false if already linked */ LinkToParentFolder: API["LinkToParentFolder"] = ( file: TFile, dryrun = false, ): boolean => { if (!isMd(file)) return false; if (file.parent) { const fnPath = this.getFolderNotePath(file.parent), shouldRun = fnPath && !this.getFolderNote(file.parent); if (shouldRun && !dryrun) { const { path } = fnPath; this.plugin.app.fileManager.renameFile(file, path); } return !!shouldRun; } else return false; }; /** * @returns return false if file not folder note */ DeleteNoteAndLinkedFolder: API["DeleteNoteAndLinkedFolder"] = ( target, dryrun = false, ) => { let file: null | TFile, folder: null | TFolder; if (target instanceof TFile) { if (!isMd(target)) return false; file = target; folder = this.getFolderFromNote(target); } else { file = this.getFolderNote(target); folder = target; } if (file && folder && !dryrun) { new DeleteWarning(this.plugin, file, folder).open(); } return !!(file && folder); }; createFolderForNoteCheck = (file: TFile) => { const result = this._createFolderForNote(file); if (!result) return false; const { folderExist, newFolderPath } = result; return !!(!folderExist && newFolderPath); }; _createFolderForNote = (file: TFile) => { if (!isMd(file)) return null; const folderForNotePath = this.getFolderPath(file, false); if ( folderForNotePath && this.vault.getAbstractFileByPath(folderForNotePath) ) { log.info("createFolderForNote(%o): already folder note", file, file.path); return null; } const newFolderPath = this.getFolderPath(file, true), folderExist = newFolderPath && this.vault.getAbstractFileByPath(newFolderPath); return { newFolderPath, folderExist }; }; /** * @returns return false if folder already exists */ createFolderForNote: API["createFolderForNote"] = async ( file: TFile, dryrun = false, ): Promise<boolean> => { const result = this._createFolderForNote(file); if (!result) return false; const { newFolderPath, folderExist } = result; if (folderExist) { log.info( "createFolderForNote(%o): target folder to create already exists", file, file.path, ); if (!dryrun) new Notice("Target folder to create already exists"); return false; } else if (!newFolderPath) { log.info( "createFolderForNote(%o): no vaild linked folder path for %s", file, file.path, ); if (!dryrun) new Notice("No vaild linked folder path for: " + file.path); } else if (!dryrun) { await this.vault.createFolder(newFolderPath); let newNotePath: string | null; switch (this.settings.folderNotePref) { case NoteLoc.Index: newNotePath = join(newFolderPath, this.settings.indexName + ".md"); break; case NoteLoc.Inside: newNotePath = join(newFolderPath, file.name); break; case NoteLoc.Outside: newNotePath = null; break; default: assertNever(this.settings.folderNotePref); } if (newNotePath) await this.plugin.app.fileManager.renameFile(file, newNotePath); } return !!(!folderExist && newFolderPath); }; // Folder Operations OpenFolderNote: API["OpenFolderNote"] = ( folder: TFolder | string, dryrun = false, config?: { newLeaf?: boolean; openViewState?: OpenViewState }, ) => { const noteResult = this.getFolderNote(folder); if (noteResult && !dryrun) { this.plugin.app.workspace.openLinkText( noteResult.path, "", config?.newLeaf, config?.openViewState, ); } return !!noteResult; }; /** * @returns return false if folder note not exists */ DeleteFolderNote: API["DeleteFolderNote"] = ( folder: TFolder, dryrun = false, ): boolean => { const noteResult = this.getFolderNote(folder); if (noteResult && !dryrun) this.vault.delete(noteResult); return !!noteResult; }; /** * @returns return false if folder note already exists */ CreateFolderNote: API["CreateFolderNote"] = ( folder: TFolder, dryrun = false, ): boolean => { let shouldRun, fnPath; if ( (shouldRun = !this.getFolderNote(folder) && (fnPath = this.getFolderNotePath(folder))) && !dryrun ) { this.vault.create(fnPath.path, this.plugin.getNewFolderNote(folder)); } return !!shouldRun; }; } class DeleteWarning extends Modal { target: TFile; targetFolder: TFolder; plugin: FNCore; constructor(plugin: FNCore, file: TFile, folder: TFolder) { super(plugin.app); this.plugin = plugin; this.target = file; this.targetFolder = folder; } get settings() { return this.plugin.settings; } deleteFolder() { let { contentEl } = this; contentEl.createEl("p", { text: "Warning: the entire folder and its content will be removed", cls: "mod-warning", }); const children = this.targetFolder.children.map((v) => v.name); contentEl.createEl("p", { text: children.length > 5 ? children.slice(0, 5).join(", ") + "..." : children.join(", "), }); contentEl.createEl("p", { text: "Continue?", cls: "mod-warning", }); const buttonContainer = contentEl.createDiv({ cls: "modal-button-container", }); buttonContainer.createEl( "button", { text: "Yes", cls: "mod-warning" }, (el) => el.onClickEvent(() => { this.app.vault.delete(this.targetFolder, true); this.app.vault.delete(this.target); this.close(); }), ); buttonContainer.createEl("button", { text: "No" }, (el) => el.onClickEvent(() => { this.close(); }), ); } onOpen() { this.containerEl.addClass("warn"); this.deleteFolder(); } onClose() { let { contentEl } = this; contentEl.empty(); } }