diff --git a/.gitignore b/.gitignore index 51ca763..f97ac34 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ data.json /package-lock.json /.tgitconfig /src/version_info.txt + +# exclude compiled .js in tests +/tests/pathExclusion.js diff --git a/src/main.ts b/src/main.ts index 8c9e846..f494c09 100644 --- a/src/main.ts +++ b/src/main.ts @@ -119,6 +119,7 @@ interface Settings { dark: number; light: number; } + excludedDirectories: string[], openInCurrentTab: boolean; enableTagsReaction: boolean; enableAutoFocus: boolean; @@ -198,7 +199,8 @@ const DEFAULT_SETTINGS: Settings = { structuredClone(DEFAULT_DISPLAY_SETTINGS_LIGHT), structuredClone(DEFAULT_DISPLAY_SETTINGS_LIGHT), structuredClone(DEFAULT_DISPLAY_SETTINGS_LIGHT), - ] + ], + excludedDirectories: [] } // plugin 主体 export default class TagsRoutes extends Plugin { @@ -365,6 +367,9 @@ export default class TagsRoutes extends Plugin { DebugMsg(DebugLevel.INFO,`New installation: Using default settings.`) } } + if (!this.settings.excludedDirectories) { + this.settings.excludedDirectories = []; + } this.settings.customSlot = this.settings[this.settings.currentTheme]; this.settings.currentSlotNum = this.settings.themeSlotNum[this.settings.currentTheme]; this.settings.customSlot[0] = structuredClone( @@ -704,5 +709,26 @@ class TagsroutesSettingsTab extends PluginSettingTab { this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Normal", "linkParticleColor")); this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Highlight", "linkParticleHighlightColor")); this.plugin.skipSave = false; + + containerEl.createEl('h2', { text: 'Directory Exclusions' }); + + new Setting(containerEl) + .setName('Excluded Directories') + .setDesc('Enter directory paths to exclude from the graph, separated by commas (e.g., "Private Notes, Archive")') + .addText(text => text + .setPlaceholder('directory1, directory2') + .setValue(this.plugin.settings.excludedDirectories.join(', ')) + .onChange(async (value) => { + this.plugin.settings.excludedDirectories = value + .split(',') + .map(dir => dir.trim()) + .filter(dir => dir.length > 0); + await this.plugin.saveSettings(); + + // Refresh the graph if it's open + if (this.plugin.view) { + this.plugin.view.onResetGraph(); + } + })); } } diff --git a/src/util/util.ts b/src/util/util.ts index 99f3a65..19e5244 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -58,6 +58,25 @@ export function createFolderIfNotExists(folderPath: string) { // DebugMsg(DebugLevel.DEBUG,`Folder already exists: ${folderPath}`); } } + + // Allow for users to exclude directories within Vault from graph indexing +export function isPathExcluded(path: string, excludedDirs: string[]): boolean { + return excludedDirs.some(dir => { + // Normalize paths to handle different path separators | Remove trailing slashes + const normalizedDir = dir.replace(/\\/g, '/').toLowerCase().replace(/\/+$/, ''); + const normalizedPath = path.replace(/\\/g, '/').toLowerCase(); + + // Handle wildcard matching (e.g., MyNotes/*) + if (normalizedDir.endsWith('/*')) { + // Remove /* to get the base directory + const baseDir = normalizedDir.slice(0, -2); + return normalizedPath === baseDir || normalizedPath.startsWith(baseDir + '/'); + } + + // Check for exact matches | exclude subdirectories of excluded directory + return normalizedPath === normalizedDir || normalizedPath.startsWith(normalizedDir + '/'); + }); +}; // 函数:获取所有标签 export const getTags = (cache: CachedMetadata | null): TagCache[] => { if (!cache || !cache.tags) return []; diff --git a/src/views/TagsRoutes.ts b/src/views/TagsRoutes.ts index 42ffdca..86adba4 100644 --- a/src/views/TagsRoutes.ts +++ b/src/views/TagsRoutes.ts @@ -1,7 +1,7 @@ import { moment, MarkdownView, Notice, CachedMetadata, ValueComponent, Platform, View } from 'obsidian'; import { ItemView, WorkspaceLeaf, TFile } from "obsidian"; import * as THREE from 'three'; -import { getFileType, getTags, parseTagHierarchy, filterStrings, shouldRemove, setViewType, showFile, DebugMsg, DebugLevel, createFolderIfNotExists } from "../util/util" +import { getFileType, getTags, parseTagHierarchy, filterStrings, shouldRemove, setViewType, showFile, DebugMsg, DebugLevel, createFolderIfNotExists, isPathExcluded } from "../util/util" import ForceGraph3D, { ForceGraph3DInstance } from "3d-force-graph"; import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'; import * as d3 from 'd3-force-3d'; @@ -1634,17 +1634,37 @@ export class TagRoutesView extends ItemView { const tagCount: Map = new Map(); // 初始化标签计数对象 const frontmatterTagCount: Map = new Map(); // 初始化标签计数对象 // 添加resolved links来创建文件间的关系,和文件节点 + // Set to track unique node ids + const nodeSet: Set = new Set(); + const nodeList: Array<{ id: string, type: string }> = []; + const linkList: Array<{ source: string, target: string, sourceId: string, targetId: string }> = []; + + // Iterate over each source path in resolvedLink object for (const sourcePath in resolvedLinks) { - if (!nodes.some(node => node.id == sourcePath)) { + // check source paths against excludedDirectories + if (isPathExcluded(sourcePath, this.plugin.settings.excludedDirectories)) { + // -> skip iteration if found + continue; + } + // Add sourcePath to nodes if not present + if (!nodeSet.has(sourcePath)) { nodes.push({ id: sourcePath, type: getFileType(sourcePath) }); + nodeSet.add(sourcePath); } const targetPaths = resolvedLinks[sourcePath]; + // Iterate over each target path to process from sourcePath for (const targetPath in targetPaths) { - // 确保目标文件也在图中 - if (!nodes.some(node => node.id == targetPath)) { + // Check against already indexed resolvedLinks + if (isPathExcluded(targetPath, this.plugin.settings.excludedDirectories)) { + continue; + } + // Add targetPath to nodes if not present + if (!nodeSet.has(targetPath)) { nodes.push({ id: targetPath, type: getFileType(targetPath) }); + nodeSet.add(targetPath); } - // 创建链接 + + // link sourcePath and targetPath links.push({ source: sourcePath, target: targetPath, sourceId: sourcePath, targetId: targetPath }); } } @@ -2015,7 +2035,6 @@ export class TagRoutesView extends ItemView { .add({ arg: (new settingGroup(this.plugin, "button-box", "button-box", "normal-box") .addButton("Apply Theme Color", "graph-button", () => { this.applyThemeColor() }) - ) }) .add({ arg: (new settingGroup(this.plugin, "button-box", "button-box", "flex-box") diff --git a/tests/pathExclusion.ts b/tests/pathExclusion.ts new file mode 100644 index 0000000..202f274 --- /dev/null +++ b/tests/pathExclusion.ts @@ -0,0 +1,36 @@ +export function isPathExcluded(path: string, excludedDirs: string[]): boolean { + return excludedDirs.some(dir => { + const normalizedDir = dir.replace(/\\/g, '/').toLowerCase().replace(/\/+$/, ''); + const normalizedPath = path.replace(/\\/g, '/').toLowerCase(); + + // Handle wildcard matching (e.g., MyNotes/*) + if (normalizedDir.endsWith('/*')) { + const baseDir = normalizedDir.slice(0, -2); + return normalizedPath === baseDir || normalizedPath.startsWith(baseDir + '/'); + } + + // Check for exact matches or if the path is within the excluded directory + return normalizedPath === normalizedDir || normalizedPath.startsWith(normalizedDir + '/'); + }); +} + + +const testCases = [ + { path: 'MyNotes/Section1/Notes', excludedDirs: ['MyNotes/*'], expected: true }, + { path: 'Logs', excludedDirs: ['Logs'], expected: true }, + { path: 'Logs/Archive', excludedDirs: ['Logs'], expected: true }, + { path: 'NotesArchive', excludedDirs: ['Notes'], expected: false }, + { path: 'Private/Notes', excludedDirs: ['Private'], expected: true }, + { path: 'Projects/Important', excludedDirs: ['Projects/*'], expected: true }, + { path: 'Projects', excludedDirs: ['Projects/*'], expected: true }, + { path: 'Reports/2024', excludedDirs: ['Reports/*'], expected: true }, + { path: 'Reports', excludedDirs: ['Archives'], expected: false } +]; + +testCases.forEach(({ path, excludedDirs, expected }, index) => { + const result = isPathExcluded(path, excludedDirs); + console.log(`Test ${index + 1}: Path "${path}" | ExcludedDirs: [${excludedDirs.join(', ')}] | Expected: ${expected} | Result: ${result}`); + console.assert(result === expected, `Test ${index + 1} failed`); +}); + +console.log('All tests completed.'); diff --git a/tsconfig.json b/tsconfig.json index 2d6fbdf..741c664 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,21 +4,19 @@ "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", - "target": "ES6", + "target": "ES2015", // Ensures support for modern string methods "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", "importHelpers": true, "isolatedModules": true, - "strictNullChecks": true, + "strictNullChecks": true, "lib": [ "DOM", - "ES5", - "ES6", - "ES7" + "ES2015" // Includes ES6 and above features ] }, "include": [ "**/*.ts" ] -} +} \ No newline at end of file