feat: added exclusion rules for folders and files with regex support

This commit is contained in:
Al0cam 2025-04-13 20:22:20 +02:00
parent d615af51e1
commit d1dedf2db1
7 changed files with 212 additions and 53 deletions

13
Models/ExclusionRule.ts Normal file
View file

@ -0,0 +1,13 @@
/**
* Defines the Exlclusion rules that will jump over the folders defined in the regex
*/
export class ExclusionRule {
/**
* This filed is used for defining the regex with its group matchers
*/
public regex: string;
constructor(regex?: string) {
this.regex = regex || "";
}
}

View file

@ -81,7 +81,8 @@ Thank you!
## Future plans
- [ ] Add excluded folder support
- [ ] Add excluded file support
- [x] Add excluded folder support
- [x] Add excluded file support
- [x] Add regex support for excluded folders and files (must support language accents like ñ, á, š, đ, こ, 猫, etc.)
- [ ] Add a file like .gitignore which contains all the moving rules (i am assuming the list can grow quite big for some people)

View file

@ -1,36 +1,27 @@
import AutoMoverPlugin from "main";
import { MovingRule } from "Models/MovingRule";
import type AutoMoverPlugin from "main";
import type { ExclusionRule } from "Models/ExclusionRule";
import type { MovingRule } from "Models/MovingRule";
export interface AutoMoverSettings {
moveOnOpen: boolean,
moveOnSave: boolean,
moveOnClose: boolean,
moveOnCreate: boolean,
movingRules: MovingRule[],
moveOnOpen: boolean;
moveOnSave: boolean;
moveOnClose: boolean;
moveOnCreate: boolean;
movingRules: MovingRule[];
excludedFolders: ExclusionRule[];
}
// TODO: Define a value which accepts a regex that will be used to manage files
// the matcher group should accept an argument which will be the name of the folder to which the files will be moved
// e.g. if the file is a daily note like 14.09.2021.md, then the folder structure should be YYYY/MM/DD
// there should be a button in the settings to sort all the files according to given criteria
// the button should be disabled if the regex is invalid -> there are no matches
// regex that should be accepted:
// YYYY -> year
// MM -> month
// DD -> day
// some string -> the string should be the name of the folder
export const DEFAULT_SETTINGS: Partial<AutoMoverSettings> = {
moveOnOpen: true,
moveOnSave: true,
moveOnClose: false,
moveOnCreate: false,
movingRules: [],
}
excludedFolders: [],
};
function loadSettings(AutoMoverPlugin: AutoMoverPlugin): Partial<AutoMoverSettings> {
function loadSettings(
AutoMoverPlugin: AutoMoverPlugin,
): Partial<AutoMoverSettings> {
return Object.assign({}, DEFAULT_SETTINGS, AutoMoverPlugin.loadData());
}
}

View file

@ -1,4 +1,5 @@
import type AutoMoverPlugin from "main";
import { ExclusionRule } from "Models/ExclusionRule";
import { MovingRule } from "Models/MovingRule";
import * as obsidian from "obsidian";
@ -50,6 +51,7 @@ export class SettingsTab extends obsidian.PluginSettingTab {
// }),
// );
// TUTORIAL START
/**
* Instead of using the default .setting-item class I created a custom class to add other styling,
* with the inclusion of the relevant styles from the .setting-item class
@ -57,9 +59,7 @@ export class SettingsTab extends obsidian.PluginSettingTab {
const tutorialContainer = containerEl.createDiv({
cls: "moving_rules_container",
});
const headerSetting = new obsidian.Setting(tutorialContainer)
.setName("Tutorial")
.setHeading();
new obsidian.Setting(tutorialContainer).setName("Tutorial").setHeading();
/**
* Rule description and tutorial
*/
@ -97,8 +97,10 @@ export class SettingsTab extends obsidian.PluginSettingTab {
});
const example2 = description.createDiv({ cls: "rule margig_right" });
example2.createSpan({ text: "Scroll (\\d+)", cls: "rule_title" });
example2.createSpan({ text: "Scrolls\/$1", cls: "rule_title" });
example2.createSpan({ text: "Scrolls/$1", cls: "rule_title" });
// TUTORIAL END
// MOVING RULES START
/**
* Header of the rules
*/
@ -121,11 +123,11 @@ export class SettingsTab extends obsidian.PluginSettingTab {
cls: "rule_title",
});
const addButton = ruleHeader.createEl("button", {
const addRuleButton = ruleHeader.createEl("button", {
text: "+",
cls: "rule_button",
});
addButton.addEventListener("click", () => {
addRuleButton.addEventListener("click", () => {
this.plugin.settings.movingRules.push(new MovingRule());
// this is used to rerender the settings tab
this.display();
@ -153,26 +155,97 @@ export class SettingsTab extends obsidian.PluginSettingTab {
this.plugin.saveData(this.plugin.settings);
};
const duplicateButton = child.createEl("button", {
const duplicateRuleButton = child.createEl("button", {
text: "⿻",
cls: "rule_button rule_button_duplicate",
});
duplicateButton.addEventListener("click", () => {
duplicateRuleButton.addEventListener("click", () => {
this.plugin.settings.movingRules.push(
new MovingRule(rule.regex, rule.folder),
);
this.display();
});
const deleteButton = child.createEl("button", {
const deleteRuleButton = child.createEl("button", {
text: "x",
cls: "rule_button rule_button_remove",
});
deleteButton.addEventListener("click", () => {
deleteRuleButton.addEventListener("click", () => {
this.plugin.settings.movingRules =
this.plugin.settings.movingRules.filter((r) => r !== rule);
this.display();
});
}
// MOVING RULES END
// EXCLUSION FOLDERS START
/**
* Header for excluded folders
*/
const excludedFoldersContainer = containerEl.createDiv({
cls: "moving_rules_container",
});
new obsidian.Setting(excludedFoldersContainer)
.setName("Exclusion rules")
.setHeading();
const exclusionList = excludedFoldersContainer.createDiv({
cls: "rule_list",
});
const exclusionHeader = exclusionList.createDiv({
cls: "rule margig_right",
});
exclusionHeader.createEl("p", {
text: "Excluded folders or files (string or regex)",
cls: "rule_title",
});
const addExclusionButton = exclusionHeader.createEl("button", {
text: "+",
cls: "rule_button",
});
addExclusionButton.addEventListener("click", () => {
this.plugin.settings.excludedFolders.push(new ExclusionRule());
this.display();
});
/**
* List of excluded folders
*/
for (const exclusion of this.plugin.settings.excludedFolders) {
const child = exclusionList.createDiv({ cls: "rule" });
child.createEl("input", {
value: exclusion.regex,
cls: "rule_input",
}).onchange = (e) => {
exclusion.regex = (e.target as HTMLInputElement).value;
this.plugin.settings.excludedFolders.map((ef) =>
ef === exclusion ? exclusion : ef,
);
this.plugin.saveData(this.plugin.settings);
};
const duplicateExclusionButton = child.createEl("button", {
text: "⿻",
cls: "rule_button rule_button_duplicate",
});
duplicateExclusionButton.addEventListener("click", () => {
this.plugin.settings.excludedFolders.push(
new ExclusionRule(exclusion.regex),
);
this.display();
});
const deleteExclusionButton = child.createEl("button", {
text: "x",
cls: "rule_button rule_button_remove",
});
deleteExclusionButton.addEventListener("click", () => {
this.plugin.settings.excludedFolders =
this.plugin.settings.excludedFolders.filter((r) => r !== exclusion);
this.display();
});
}
// EXCLUSION FOLDERS END
}
}

View file

@ -0,0 +1,37 @@
import type { ExclusionRule } from "Models/ExclusionRule";
import type { TFile } from "obsidian";
class ExclusionMatcherUtil {
private static instance: ExclusionMatcherUtil;
private constructor() {}
public static getInstance(): ExclusionMatcherUtil {
if (!ExclusionMatcherUtil.instance) {
ExclusionMatcherUtil.instance = new ExclusionMatcherUtil();
}
return ExclusionMatcherUtil.instance;
}
/**
* This method is used to check if the file is excluded by the exclusion rule
* @param file - The file to be checked
* @param exclusionRules - The exclusion rule to be used
* @returns true if the file path is excluded, false otherwise
*/
isFilePathExcluded(file: TFile, exclusionRules: ExclusionRule[]): boolean {
console.log("Exclusion rules: ", exclusionRules);
for (const rule of exclusionRules) {
const regex = new RegExp(rule.regex);
console.log("Regex: ", regex);
console.log("File path: ", file.path);
console.log("Result: ", regex.test(file.path));
if (regex.test(file.path)) {
return true;
}
}
return false;
}
}
const exclusionMatcherUtil = ExclusionMatcherUtil.getInstance();
export default exclusionMatcherUtil;

View file

@ -27,6 +27,10 @@ class RuleMatcherUtil {
console.error("Rule does not have a regex: ", rule);
continue;
}
if (rule.folder == null || rule.folder === "") {
console.error("Rule does not have a destination folder: ", rule);
continue;
}
if (!this.isValidRegex(rule.regex)) {
console.error("Rule has an invalid regex: ", rule);
continue;

74
main.ts
View file

@ -3,6 +3,7 @@ import { SettingsTab } from "Settings/SettingsTab";
import movingUtil from "Utils/MovingUtil";
import ruleMatcherUtil from "Utils/RuleMatcherUtil";
import * as obsidian from "obsidian";
import exclusionMatcherUtil from "Utils/ExclusionMatcherUtil";
export default class AutoMoverPlugin extends obsidian.Plugin {
settings: Settings.AutoMoverSettings;
@ -19,18 +20,20 @@ export default class AutoMoverPlugin extends obsidian.Plugin {
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: obsidian.TFile) => {
this.matchAndMoveFile(file);
}),
);
}
// negative ifs for easier reading and debugging
if (!this.areMovingTriggersEnabled()) return;
if (!this.areThereRulesToApply()) return;
if (!this.areRulesValid()) return;
if (this.settings.moveOnOpen) {
this.registerEvent(
this.app.workspace.on("file-open", (file: obsidian.TFile) => {
if (file == null || file.path == null) return;
if (this.isFileExcluded(file)) return;
this.matchAndMoveFile(file);
}),
);
}
}
@ -47,6 +50,22 @@ export default class AutoMoverPlugin extends obsidian.Plugin {
new obsidian.Notice("All files moved!", 5000);
}
/**
* Checks if the file is excluded by the exclusion rule
*
* @param file - The file to be checked
* @returns true if the file path is excluded, false otherwise
*/
isFileExcluded(file: obsidian.TFile): boolean {
if (!this.areThereExcludedFolders()) return false;
if (!this.areThereRulesToApply()) return false;
return exclusionMatcherUtil.isFilePathExcluded(
file,
this.settings.excludedFolders,
);
}
/**
* Matches the file to the rule and moves it to the destination folder
*
@ -54,7 +73,7 @@ export default class AutoMoverPlugin extends obsidian.Plugin {
* @returns void
*/
matchAndMoveFile(file: obsidian.TFile): void {
if (file == null || file.path == null) return;
console.log("Moving file: ", file.path);
const rule = ruleMatcherUtil.getMatchingRule(
file,
this.settings.movingRules,
@ -88,7 +107,7 @@ export default class AutoMoverPlugin extends obsidian.Plugin {
* No point in doing anything if there is no trigger set which will cause you to move the files
* @returns boolean
*/
checkIfMovingTriggersAreEnabled(): boolean {
areMovingTriggersEnabled(): boolean {
return (
this.settings.moveOnClose ||
this.settings.moveOnCreate ||
@ -101,17 +120,38 @@ export default class AutoMoverPlugin extends obsidian.Plugin {
* 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;
areThereRulesToApply(): boolean {
return (
this.settings.movingRules.length > 0 &&
this.settings.movingRules.every(
(rule) => rule.regex !== "" && rule.folder !== "",
)
);
}
/**
* If there are no excluded folders, then we can move thing freely
* @returns boolean
*/
areThereExcludedFolders(): boolean {
return this.settings.excludedFolders.length > 0;
}
/**
* Superficail check if the rules are valid
* @returns boolean
*/
checkIfRulesAreValid(): boolean {
areRulesValid(): boolean {
return this.settings.movingRules.every(
(rule) => rule.regex !== "" && rule.folder !== "",
);
}
/**
* Superficail check if the excluded folders are valid
* @returns boolean
*/
areExcludedFoldersValid(): boolean {
return this.settings.excludedFolders.every((rule) => rule.regex !== "");
}
}