From fedc3210a602005af9e3c6c306e2517afa1ecb07 Mon Sep 17 00:00:00 2001 From: Al0cam Date: Mon, 10 Feb 2025 12:01:16 +0100 Subject: [PATCH] finished plugin core functions --- Utils/MovingUtil.ts | 197 +++++++++++++++++++++++---------------- Utils/RuleMatcherUtil.ts | 185 +++++++++++++++++++++--------------- main.ts | 158 ++++++++++++++----------------- 3 files changed, 296 insertions(+), 244 deletions(-) diff --git a/Utils/MovingUtil.ts b/Utils/MovingUtil.ts index a49a840..3e99ce9 100644 --- a/Utils/MovingUtil.ts +++ b/Utils/MovingUtil.ts @@ -1,92 +1,133 @@ -import { MovingRule } from "Models/MovingRule"; import { App, Notice, TFile, TFolder } from "obsidian"; class MovingUtil { - private static instance: MovingUtil; - private app: App; + private static instance: MovingUtil; + private app: App; - private constructor() {} + private constructor() { } - public static getInstance(): MovingUtil { - if (!MovingUtil.instance) { - MovingUtil.instance = new MovingUtil(); - } - return MovingUtil.instance; - } + public static getInstance(): MovingUtil { + if (!MovingUtil.instance) { + MovingUtil.instance = new MovingUtil(); + } + return MovingUtil.instance; + } - public init(app: App): void { - this.app = app; - } + public init(app: App): void { + this.app = app; + } - /** - * Checks if the path is a file - * - * @param path - Path to be checked - * @returns boolean - */ - public isFile(path: string): boolean { - const file = this.app.vault.getAbstractFileByPath(path); - return file instanceof TFile; - } + /** + * Checks if the path is a file + * + * @param path - Path to be checked + * @returns boolean + */ + public isFile(path: string): boolean { + const file = this.app.vault.getAbstractFileByPath(path); + return file instanceof TFile; + } - /** - * Checks if the path is a folder - * - * @param path - Path to be checked - * @returns boolean - */ - public isFolder(path: string): boolean { - return this.app.vault.getAbstractFileByPath(path) instanceof TFolder; - } + /** + * Checks if the path is a folder + * + * @param path - Path to be checked + * @returns boolean + */ + public isFolder(path: string): boolean { + return this.app.vault.getAbstractFileByPath(path) instanceof TFolder; + } - /** - * Moves the file to the destination path - * - * @param file - File to be moved - * @param newPath - Destination path - * @returns void - */ - public moveFile(file: TFile, newPath: string): void { - if (this.isFolder(newPath)) { - this.app.vault.rename(file, `${newPath}/${file.name}`); - } else { - new Notice(`Invalid destination path\n${newPath} is not a folder!`, 5000); - console.error(`Invalid destination path\n${newPath} is not a folder!`); - } - } + /** + * Moves the file to the destination path + * + * @param file - File to be moved + * @param newPath - Destination path + * @returns void + */ + public moveFile(file: TFile, newPath: string): void { + if (this.isFolder(newPath)) { + this.app.vault.rename(file, `${newPath}/${file.name}`); + } else { + new Notice( + `Invalid destination path\n${newPath} is not a folder!\nCreating requested folder.`, + 5000, + ); + console.error( + `Invalid destination path\n${newPath} is not a folder!\nCreating requested folder.`, + ); + this.createMissingFolders(newPath).then(() => { + this.app.vault.rename(file, `${newPath}/${file.name}`); + }); + } + } - /** - * Moves the file to the destination path - * - * @param folder - Folder to be moved - * @param newPath - Destination path - * @returns void - */ - public moveFolder(folder: TFolder, newPath: string): void { - if (this.isFolder(newPath)) { - this.app.vault.rename(folder, `${newPath}/${folder.name}`); - } else { - new Notice(`Invalid destination path\n${newPath} is not a folder!`, 5000); - console.error(`Invalid destination path\n${newPath} is not a folder!`); - } - } + /** + * Moves the folder to the destination path + * + * @param folder - Folder to be moved + * @param newPath - Destination path + * @returns void + */ + public moveFolder(folder: TFolder, newPath: string): void { + if (this.isFolder(newPath)) { + this.app.vault.rename(folder, `${newPath}/${folder.name}`); + } else { + new Notice(`Invalid destination path\n${newPath} is not a folder!`, 5000); + console.error(`Invalid destination path\n${newPath} is not a folder!`); + } + } - /** - * Creates folder in destination path if it does not exist already - * - * @param path - Path of the folder to be created - * @returns Created folder or null if folder already exists - */ - public createFolder(path: string): TFolder | null { - if (!this.isFolder(path)) { - this.app.vault.createFolder(path).then((folder) => { - return folder; - }); - } - new Notice("Folder already exists", 5000); - console.error("Folder already exists"); - return null; - } + /** + * Creates folder in destination path if it does not exist already + * + * @param path - Path of the folder to be created + * @returns Created folder or null if folder already exists + */ + public createFolder(path: string): TFolder | null { + if (!this.isFolder(path)) { + this.app.vault.createFolder(path).then((folder) => { + return folder; + }); + } else { + new Notice("Folder already exists", 5000); + console.error("Folder already exists"); + } + return null; + } + + /** + * Splits the path into an array of strings + * + * @param path - Path to be split + * @returns string[] + */ + private splitPath(path: string): string[] { + return path.split("/"); + } + + /** + * Create missing folders in the path + * + * @param path - Path to be checked + * @returns void + */ + private async createMissingFolders(path: string): Promise { + const splitPath = this.splitPath(path); + let currentPath = ""; + for (const folder of splitPath) { + currentPath += folder; + if (!this.isFolder(currentPath)) { + await this.app.vault.createFolder(path); + } + currentPath += "/"; + } + if (this.isFolder(path)) { + return true; + } + + return false; + } } const movingUtil = MovingUtil.getInstance(); diff --git a/Utils/RuleMatcherUtil.ts b/Utils/RuleMatcherUtil.ts index 3bfc2f5..02d95d8 100644 --- a/Utils/RuleMatcherUtil.ts +++ b/Utils/RuleMatcherUtil.ts @@ -2,92 +2,123 @@ import type { MovingRule } from "Models/MovingRule"; import type { TFile } from "obsidian"; class RuleMatcherUtil { - private static instance: RuleMatcherUtil; + private static instance: RuleMatcherUtil; - private constructor() {} + private constructor() { } - public static getInstance(): RuleMatcherUtil { - if (!RuleMatcherUtil.instance) { - RuleMatcherUtil.instance = new RuleMatcherUtil(); - } - return RuleMatcherUtil.instance; - } + public static getInstance(): RuleMatcherUtil { + if (!RuleMatcherUtil.instance) { + RuleMatcherUtil.instance = new RuleMatcherUtil(); + } + return RuleMatcherUtil.instance; + } - /** - * Returns the first rule that matches the file - * If no rule matches, returns null - * - * @param file - * @param rules - * @returns MovingRule | null - */ - public getMatchingRule(file: TFile, rules: MovingRule[]): MovingRule | null { - for(const rule of rules) { - if(rule.regex == null || rule.regex === "") { - console.error("Rule does not have a regex: ", rule); - continue; - } else if(!this.isValidRegex(rule.regex)) { - console.error("Rule has an invalid regex: ", rule); - continue; - } - console.log("Checking if matches: ", file.name.match(rule.regex)); - console.log("Rule: ", rule); - if(file.name.match(rule.regex)) { - return rule; - } - } - return null; - } + /** + * Returns the first rule that matches the file + * If no rule matches, returns null + * + * @param file + * @param rules + * @returns MovingRule | null + */ + public getMatchingRule(file: TFile, rules: MovingRule[]): MovingRule | null { + for (const rule of rules) { + if (rule.regex == null || rule.regex === "") { + console.error("Rule does not have a regex: ", rule); + continue; + } + if (!this.isValidRegex(rule.regex)) { + console.error("Rule has an invalid regex: ", rule); + continue; + } + if (file.name.match(rule.regex)) { + return rule; + } + } + return null; + } - /** - * Returns the regex groups of the file that match the rule - * If no groups match, returns an empty array - * - * @param file - * @param rule - * @returns string[] - */ - public getGroupMatches(file: TFile, rule: MovingRule): RegExpMatchArray | null { - const matches = file.name.match(rule.regex); - return matches; - } + /** + * Returns the regex groups of the file that match the rule + * If no groups match, returns an empty array + * + * @param file + * @param rule + * @returns string[] + */ + public getGroupMatches( + file: TFile, + rule: MovingRule, + ): RegExpMatchArray | null { + const matches = file.name.match(rule.regex); + return matches; + } - /** - * Checks if the regex pattern is valid - * Meaning, syntax errors are caught, but not logical errors - * - * @param pattern - * @returns boolean - */ - private isValidRegex(pattern: string): boolean { - try { - new RegExp(pattern); - return true; - } catch (e) { - if (e instanceof SyntaxError) { - console.error(`Invalid regex pattern: ${e.message}`); - } - return false; - } - } + /** + * Checks if the regex pattern is valid + * Meaning, syntax errors are caught, but not logical errors + * + * @param pattern + * @returns boolean + */ + private isValidRegex(pattern: string): boolean { + try { + new RegExp(pattern); + return true; + } catch (e) { + if (e instanceof SyntaxError) { + console.error(`Invalid regex pattern: ${e.message}`); + } + return false; + } + } - // TODO: implement the distinction between named and unnamed groups - public constructFinalDesinationPath(rule: MovingRule, matches: RegExpMatchArray): string { - const folderPath = rule.folder; - return folderPath; - } + public constructFinalDesinationPath( + rule: MovingRule, + matches: RegExpMatchArray, + ): string { + let folderPath = rule.folder; + const unnamedGroups = this.getUnnamedGroups(rule.regex); + for (let i = 0; i < unnamedGroups!.length; i++) { + folderPath = folderPath.replace(`$${i + 1}`, matches[i + 1] ?? ""); + } + return folderPath; + } - private hasUnnamedGroups(pattern: string): boolean { - return /\(/.test(pattern); - } + /** + * Is regex grouped + * Groups are defined by (...) and used with $1, $2, $3, etc. + * + * @param rule + * @returns boolean + */ + public isRegexGrouped(rule: MovingRule): boolean { + return ( + this.doesDestinationUseGroups(rule) && + this.getUnnamedGroups(rule.regex) != null + ); + } - private getNamedGroups(pattern: string): RegExpMatchArray | null { - return pattern.match(/\(\?<\w+>/g); - } + /** + * Returns the unnamed groups in the regex pattern + * + * @param pattern + * @returns RegExpMatchArray | null + */ + private getUnnamedGroups(pattern: string): RegExpMatchArray | null { + return pattern.match(/\(/g); + } - private getUnnamedGroups(pattern: string): RegExpMatchArray | null { - return pattern.match(/\(/g); - } + /** + * Does destination use groups + * Groups are defined by $1, $2, $3, etc. + * + * @param rule + * @returns boolean + */ + private doesDestinationUseGroups(rule: MovingRule): boolean { + return /\$\d/.test(rule.folder); + } } const ruleMatcherUtil = RuleMatcherUtil.getInstance(); diff --git a/main.ts b/main.ts index bd8c1af..87dbc06 100644 --- a/main.ts +++ b/main.ts @@ -5,99 +5,79 @@ import movingUtil from "Utils/MovingUtil"; import ruleMatcherUtil from "Utils/RuleMatcherUtil"; export default class AutoMoverPlugin extends Plugin { - settings: AutoMoverSettings; + settings: AutoMoverSettings; - async onload() { - console.log("loading plugin"); - movingUtil.init(this.app); - /** - * Loading settings and creating the settings tab - */ - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - this.addSettingTab(new SettingsTab(this.app, this)); - console.log(this.settings); + async onload() { + // console.log("loading plugin"); + movingUtil.init(this.app); + /** + * Loading settings and creating the settings tab + */ + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + this.addSettingTab(new SettingsTab(this.app, this)); + if ( + this.checkIfMovingTriggersAreEnabled() && + this.checkIfThereAreRulesToApply() && + this.checkIfRulesAreValid() + ) { + if (this.settings.moveOnOpen) { + this.registerEvent( + this.app.workspace.on("file-open", (file: TFile) => { + if (file == null || file.path == null) return; + const rule = ruleMatcherUtil.getMatchingRule( + file, + this.settings.movingRules, + ); + if (rule == null || rule.folder == null) return; + if (ruleMatcherUtil.isRegexGrouped(rule)) { + const matches = ruleMatcherUtil.getGroupMatches(file, rule); + const finalDestinationPath = + ruleMatcherUtil.constructFinalDesinationPath(rule, matches!); + movingUtil.moveFile(file, finalDestinationPath); + } else { + movingUtil.moveFile(file, rule.folder); + } + }), + ); + } + } } - // Statement: check if there are any settings on when to apply the rules, - // because if you arent doing on open, save, close or create, then there is no point in checking for rules - if ( - this.checkIfMovingTriggersAreEnabled() && - this.checkIfThereAreRulesToApply() && - this.checkIfRulesAreValid() - ) { - if (this.settings.moveOnOpen) { - this.registerEvent( - this.app.workspace.on("file-open", (file: TFile) => { - if (file == null || file.path == null) return; - console.log("File opened: ", file.path); - // Question: what if there are multiple rules that apply to the same file? - // so far only the first one is applied and the rest are ignored - // i can live with that + async asyncloadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } - // Question: what if the file is already in the correct folder? - // do nothing... + async onunload() { + // console.log("unloading plugin"); + } - // TODO: what if the folder which the file is supposed to be moved to does not exist? - // create the whole path to the folder and the folder itself - // having it do nothing would be conflicting with the idea to use regex groups in the first place + /** + * No point in doing anything if there is no trigger set which will cause you to move the files + * @returns boolean + */ + checkIfMovingTriggersAreEnabled(): boolean { + return ( + this.settings.moveOnClose || + this.settings.moveOnCreate || + this.settings.moveOnOpen || + this.settings.moveOnSave + ); + } - const rule = ruleMatcherUtil.getMatchingRule( - file, - this.settings.movingRules, - ); + /** + * If there are no rules to apply, then there is no point in checking for them + * @returns boolean + */ + checkIfThereAreRulesToApply(): boolean { + return this.settings.movingRules.length > 0; + } - if (rule != null) { - // TODO: Construct the folder path using the regex groups - const matches = ruleMatcherUtil.getGroupMatches(file, rule); - console.log("Matches: ", matches); - console.log("Moving file to: ", rule.folder); - // movingUtil.moveFile(file, rule.folder); - } - }), - ); - } else { - // TODO: create event listener which happens on file save - // scratched feature - // maybe requires definition of new event and listener - } - } - } - - async asyncloadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } - - async onunload() { - console.log("unloading plugin"); - } - - /** - * No point in doing anything if there is no trigger set which will cause you to move the files - * @returns boolean - */ - checkIfMovingTriggersAreEnabled(): boolean { - return ( - this.settings.moveOnClose || - this.settings.moveOnCreate || - this.settings.moveOnOpen || - this.settings.moveOnSave - ); - } - - /** - * If there are no rules to apply, then there is no point in checking for them - * @returns boolean - */ - checkIfThereAreRulesToApply(): boolean { - return this.settings.movingRules.length > 0; - } - - /** - * Superficail check if the rules are valid - * @returns boolean - */ - checkIfRulesAreValid(): boolean { - return this.settings.movingRules.every( - (rule) => rule.regex !== "" && rule.folder !== "", - ); - } + /** + * Superficail check if the rules are valid + * @returns boolean + */ + checkIfRulesAreValid(): boolean { + return this.settings.movingRules.every( + (rule) => rule.regex !== "" && rule.folder !== "", + ); + } }