diff --git a/src/lib/customRule.test.ts b/src/lib/customRule.test.ts index e87d6b90..ddd92ede 100644 --- a/src/lib/customRule.test.ts +++ b/src/lib/customRule.test.ts @@ -204,3 +204,132 @@ describe('getSortedRules', () => { expect(sortedRules[2].order).toBe(2); }); }); + +describe('add', () => { + let createIconNode: SpyInstance; + let plugin: any; + let rule: CustomRule; + let file: any; + beforeEach(() => { + document.body.innerHTML = ''; + vi.restoreAllMocks(); + createIconNode = vi.spyOn(dom, 'createIconNode'); + // eslint-disable-next-line @typescript-eslint/no-empty-function + createIconNode.mockImplementationOnce(() => {}); + plugin = { + app: { + vault: { + adapter: { + stat: () => ({ type: 'file' }), + }, + }, + }, + getIconNameFromPath: () => false, + }; + rule = { + for: 'everything', + rule: 'test', + icon: 'test', + order: 0, + }; + file = { + path: 'test', + name: 'test', + }; + }); + + it('should add the icon to the node', async () => { + const node = document.createElement('div'); + node.setAttribute('data-path', 'test'); + document.body.appendChild(node); + + const result = await customRule.add(plugin, rule, file); + expect(createIconNode).toBeCalledTimes(1); + expect(result).toBe(true); + }); + + it('should not add the icon to the node if the node already has an icon', async () => { + plugin.getIconNameFromPath = () => 'IbTest'; + const result = await customRule.add(plugin, rule, file); + expect(createIconNode).toBeCalledTimes(0); + expect(result).toBe(false); + }); + + it('should not add the icon to the node if the node does not match the rule', async () => { + rule.rule = 'test1'; + const result = await customRule.add(plugin, rule, file); + expect(createIconNode).toBeCalledTimes(0); + expect(result).toBe(false); + }); +}); + +describe('doesMatchPath', () => { + let rule: CustomRule; + beforeEach(() => { + rule = { + for: 'everything', + rule: 'test', + icon: 'test', + order: 0, + }; + }); + + it('should return `true` if the rule matches the path', () => { + expect(customRule.doesMatchPath(rule, 'test')).toBe(true); + rule.rule = 'test.*'; + expect(customRule.doesMatchPath(rule, 'test')).toBe(true); + }); + + it('should return `false` if the rule does not match the path', () => { + rule.rule = 'test1'; + expect(customRule.doesMatchPath(rule, 'test')).toBe(false); + rule.rule = '.*-test'; + expect(customRule.doesMatchPath(rule, 'test')).toBe(false); + }); +}); + +describe('getFileItems', () => { + it('should return the file items which are applicable to the custom rule', async () => { + const plugin = { + app: { + vault: { + adapter: { + stat: () => ({ type: 'file' }), + }, + }, + }, + getRegisteredFileExplorers: () => [ + { + fileItems: [ + { + file: { + path: 'test', + name: 'test', + }, + }, + { + file: { + path: 'foo', + name: 'foo', + }, + }, + { + file: { + path: 'test1', + name: 'test1', + }, + }, + ], + }, + ], + } as any; + const rule: CustomRule = { + for: 'everything', + rule: 'test', + icon: 'test', + order: 0, + }; + const result = await customRule.getFileItems(plugin, rule); + expect(result.length).toBe(2); + }); +}); diff --git a/src/lib/customRule.ts b/src/lib/customRule.ts index 2e0db1c5..c4f904af 100644 --- a/src/lib/customRule.ts +++ b/src/lib/customRule.ts @@ -1,10 +1,10 @@ import { Plugin, TAbstractFile } from 'obsidian'; -import emoji from '../emoji'; import IconFolderPlugin from '../main'; import { CustomRule } from '../settings/data'; import dom from './util/dom'; import { getFileItemTitleEl } from '../util'; import config from '../config'; +import { FileItem } from '../@types/obsidian'; export type CustomRuleFileType = 'file' | 'folder'; @@ -43,22 +43,14 @@ const isApplicable = async ( } const fileType = metadata.type; - const toMatch = rule.useFilePath ? file.path : file.name; const doesMatch = doesMatchFileType(rule, fileType); - try { - // Rule is in some sort of regex. - const regex = new RegExp(rule.rule); - if (!toMatch.match(regex)) { - return false; - } - - return doesMatch; - } catch { - // Rule is not in some sort of regex, check for basic string match. - return toMatch.includes(rule.rule) && doesMatch; + if (!doesMatch) { + return false; } + + return doesMatchPath(rule, file.path); }; /** @@ -88,7 +80,7 @@ const removeFromAllFiles = async ( } const fileType = (await plugin.app.vault.adapter.stat(dataPath)).type; - if (doesExistInPath(rule, dataPath) && doesMatchFileType(rule, fileType)) { + if (doesMatchPath(rule, dataPath) && doesMatchFileType(rule, fileType)) { dom.removeIconInNode(parent); } } @@ -104,44 +96,31 @@ const getSortedRules = (plugin: IconFolderPlugin): CustomRule[] => { }; /** - * Tries to apply all custom rules to all files. This function iterates over all the saved - * custom rules and calls {@link addToAllFiles}. - * @param plugin Instance of the IconFolderPlugin. - */ -const addAll = async (plugin: IconFolderPlugin): Promise => { - for (const rule of getSortedRules(plugin)) { - await addToAllFiles(plugin, rule); - } -}; - -/** - * Tries to add all specific custom rule icon to all registered files. It does that by - * calling the {@link add} function. Furthermore, it also checks whether the file or folder - * already has an icon. Custom rules should have the lowest priority and will get ignored - * if an icon already exists in the file or folder. - * @param plugin Instance of the IconFolderPlugin. - * @param rule Custom rule that will be applied, if applicable, to all files. + * Tries to add all specific custom rule icons to all registered files and directories. + * It does that by calling the {@link add} function. Custom rules should have the lowest + * priority and will get ignored if an icon already exists in the file or directory. + * @param plugin IconFolderPlugin instance. + * @param rule CustomRule that will be applied, if applicable, to all files and folders. */ const addToAllFiles = async ( plugin: IconFolderPlugin, rule: CustomRule, ): Promise => { - for (const fileExplorer of plugin.getRegisteredFileExplorers()) { - const files = Object.values(fileExplorer.fileItems); - for (const fileItem of files) { - await add(plugin, rule, fileItem.file, getFileItemTitleEl(fileItem)); - } + const fileItems = await getFileItems(plugin, rule); + for (const fileItem of fileItems) { + await add(plugin, rule, fileItem.file, getFileItemTitleEl(fileItem)); } }; /** * Tries to add the icon of the custom rule to a file or folder. This function also checks * if the file type matches the `for` property of the custom rule. - * @param plugin Instance of the IconFolderPlugin. - * @param rule Custom rule that will be used to check if the rule is applicable to the file. - * @param file File or folder that will be used to possibly create the icon for. - * @param container Optional element where the icon will be added if the custom rules matches. - * @returns A promise that resolves to true if the icon was added, false otherwise. + * @param plugin IconFolderPlugin instance. + * @param rule CustomRule that will be used to check if the rule is applicable to the file + * or directory. + * @param file TAbstractFile that will be used to possibly create the icon for. + * @param container HTMLElement where the icon will be added if the custom rules matches. + * @returns A promise that resolves to `true` if the icon was added, `false` otherwise. */ const add = async ( plugin: IconFolderPlugin, @@ -153,33 +132,19 @@ const add = async ( return false; } - // Gets the type of the file. - const fileType = (await plugin.app.vault.adapter.stat(file.path)).type; - + // Checks if the file or directory already has an icon. const hasIcon = plugin.getIconNameFromPath(file.path); - if (!doesMatchFileType(rule, fileType) || hasIcon) { + if (hasIcon) { return false; } - const toMatch = rule.useFilePath ? file.path : file.name; - try { - // Rule is in some sort of regex. - const regex = new RegExp(rule.rule); - if (toMatch.match(regex)) { - dom.createIconNode(plugin, file.path, rule.icon, { - color: rule.color, - container, - }); - return true; - } - } catch { - // Rule is not applicable to a regex format. - if (toMatch.includes(rule.rule)) { - dom.createIconNode(plugin, file.path, rule.icon, { - color: rule.color, - container, - }); - return true; - } + + const doesMatch = await isApplicable(plugin, rule, file); + if (doesMatch) { + dom.createIconNode(plugin, file.path, rule.icon, { + color: rule.color, + container, + }); + return true; } return false; @@ -191,7 +156,7 @@ const add = async ( * @param path Path to check in. * @returns True if the rule exists in the path, false otherwise. */ -const doesExistInPath = (rule: CustomRule, path: string): boolean => { +const doesMatchPath = (rule: CustomRule, path: string): boolean => { const toMatch = rule.useFilePath ? path : path.split('/').pop(); try { // Rule is in some sort of regex. @@ -208,40 +173,21 @@ const doesExistInPath = (rule: CustomRule, path: string): boolean => { }; /** - * Gets a custom rule by its path. - * @param plugin Instance of the plugin. - * @param path Path to check for. - * @returns The custom rule if it exists, undefined otherwise. - */ -const getByPath = ( - plugin: IconFolderPlugin, - path: string, -): CustomRule | undefined => { - if (path === 'settings' || path === 'migrated') { - return undefined; - } - - return getSortedRules(plugin).find( - (rule) => !emoji.isEmoji(rule.icon) && doesExistInPath(rule, path), - ); -}; - -/** - * Gets all the files and directories that can be applied to the specific custom rule. + * Gets all the file items that can be applied to the specific custom rule. * @param plugin Instance of IconFolderPlugin. * @param rule Custom rule that will be checked for. - * @returns An array of files and directories that match the custom rule. + * @returns A promise that resolves to an array of file items that match the custom rule. */ -const getFiles = ( +const getFileItems = async ( plugin: IconFolderPlugin, rule: CustomRule, -): TAbstractFile[] => { - const result: TAbstractFile[] = []; +): Promise => { + const result: FileItem[] = []; for (const fileExplorer of plugin.getRegisteredFileExplorers()) { const files = Object.values(fileExplorer.fileItems); for (const fileItem of files) { - if (doesExistInPath(rule, fileItem.file.path)) { - result.push(fileItem.file); + if (await isApplicable(plugin, rule, fileItem.file)) { + result.push(fileItem); } } } @@ -249,14 +195,12 @@ const getFiles = ( }; export default { - getFiles, - doesExistInPath, + getFileItems, + doesMatchPath, doesMatchFileType, getSortedRules, - getByPath, removeFromAllFiles, add, - addAll, addToAllFiles, isApplicable, }; diff --git a/src/lib/icon.ts b/src/lib/icon.ts index 3a124053..9a7b6676 100644 --- a/src/lib/icon.ts +++ b/src/lib/icon.ts @@ -247,7 +247,9 @@ const addAll = ( } // Handles the custom rules. - customRule.addAll(plugin); + for (const rule of customRule.getSortedRules(plugin)) { + customRule.addToAllFiles(plugin, rule); + } }; /** @@ -285,7 +287,9 @@ const getByPath = ( } // Tries to get the custom rule for the path and returns its icon if it exists. - const rule = customRule.getByPath(plugin, path); + const rule = customRule.getSortedRules(plugin).find((rule) => { + return customRule.doesMatchPath(rule, path); + }); if (rule) { return rule.icon; } diff --git a/src/main.ts b/src/main.ts index 034a82d4..09f20384 100644 --- a/src/main.ts +++ b/src/main.ts @@ -167,7 +167,7 @@ export default class IconFolderPlugin extends Plugin { const removeIconMenuItem = (item: MenuItem) => { item.setTitle('Remove icon'); item.setIcon('trash'); - item.onClick(() => { + item.onClick(async () => { this.removeFolderIcon(file.path); dom.removeIconInPath(file.path); this.notifyPlugins(); @@ -209,7 +209,33 @@ export default class IconFolderPlugin extends Plugin { }); } - customRule.addAll(this); + // Refreshes the icon tab and title icon for custom rules. + for (const rule of customRule.getSortedRules(this)) { + const applicable = await customRule.isApplicable( + this, + rule, + file, + ); + if (applicable) { + customRule.add(this, rule, file); + this.addIconInTitle(rule.icon); + const tabLeaves = iconTabs.getTabLeavesOfFilePath( + this, + file.path, + ); + for (const tabLeaf of tabLeaves) { + iconTabs.add( + this, + file as TFile, + tabLeaf.tabHeaderInnerIconEl, + { + iconName: rule.icon, + }, + ); + } + break; + } + } }); }; @@ -425,14 +451,14 @@ export default class IconFolderPlugin extends Plugin { // Removes possible icons from the renamed file. sortedRules.forEach((rule) => { - if (customRule.doesExistInPath(rule, oldPath)) { + if (customRule.doesMatchPath(rule, oldPath)) { dom.removeIconInPath(file.path); } }); // Adds possible icons to the renamed file. sortedRules.forEach((rule) => { - if (customRule.doesExistInPath(rule, oldPath)) { + if (customRule.doesMatchPath(rule, oldPath)) { return; } diff --git a/src/settings/helper.ts b/src/settings/helper.ts index 5e112c0a..c2b96f9c 100644 --- a/src/settings/helper.ts +++ b/src/settings/helper.ts @@ -9,7 +9,7 @@ import { getFileItemTitleEl } from '../util'; * sort of inheritance, or in a custom rule involved. * @param plugin Instance of the IconFolderPlugin. */ -const refreshStyleOfIcons = (plugin: IconFolderPlugin): void => { +const refreshStyleOfIcons = async (plugin: IconFolderPlugin): Promise => { // Refreshes the icon style for all normally added icons. style.refreshIconNodes(plugin); @@ -54,13 +54,12 @@ const refreshStyleOfIcons = (plugin: IconFolderPlugin): void => { // Refreshes the icon style for all custom icon rules, when the color of the rule is // not defined. for (const rule of customRule.getSortedRules(plugin)) { - const files = customRule.getFiles(plugin, rule); - for (const file of files) { + const fileItems = await customRule.getFileItems(plugin, rule); + for (const fileItem of fileItems) { if (rule.color) { continue; } - const fileItem = fileExplorer.view.fileItems[file.path]; const titleEl = getFileItemTitleEl(fileItem); const iconNode = titleEl.querySelector( '.obsidian-icon-folder-icon', diff --git a/src/settings/ui/emojiStyle.ts b/src/settings/ui/emojiStyle.ts index 10059918..74fb86a0 100644 --- a/src/settings/ui/emojiStyle.ts +++ b/src/settings/ui/emojiStyle.ts @@ -83,6 +83,8 @@ export default class EmojiStyleSetting extends IconFolderSetting { } } - customRule.addAll(this.plugin); + for (const rule of customRule.getSortedRules(this.plugin)) { + customRule.addToAllFiles(this.plugin, rule); + } } }