Skip to content

Commit 4fc596b

Browse files
authored
Merge pull request #207 from fa1k3/fix/graph-index-titles
Use parent folder titles for index graph nodes
2 parents 37dd230 + 2754545 commit 4fc596b

3 files changed

Lines changed: 137 additions & 11 deletions

File tree

src/tools/graph-search.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ export class GraphSearchTool {
274274
const paths = rawPaths.map(pathList =>
275275
pathList.map(filePath => ({
276276
path: filePath,
277-
title: filePath.replace(/\.md$/, '').split('/').pop() || filePath
277+
title: this.graphTraversal.getNodeTitleForPath(filePath)
278278
}))
279279
);
280280

@@ -324,12 +324,15 @@ export class GraphSearchTool {
324324

325325
const stats = this.graphTraversal.getNodeStatistics(params.sourcePath);
326326
const file = this.app.vault.getAbstractFileByPath(params.sourcePath);
327+
const title = file instanceof TFile
328+
? this.graphTraversal.getNodeTitle(file)
329+
: params.sourcePath;
327330

328331
return {
329332
operation: 'statistics',
330333
sourcePath: params.sourcePath,
331334
statistics: stats,
332-
message: `Link statistics for ${file?.name || params.sourcePath}`,
335+
message: `Link statistics for ${title}`,
333336
workflow: {
334337
message: 'Statistics retrieved. You can explore the actual links or find connected nodes.',
335338
suggested_next: [
@@ -371,7 +374,7 @@ export class GraphSearchTool {
371374
const cache = this.app.metadataCache.getFileCache(file);
372375
nodes.push({
373376
path: edge.source,
374-
title: file.name.replace(/\.md$/, ''),
377+
title: this.graphTraversal.getNodeTitle(file),
375378
type: 'file',
376379
tags: cache?.tags?.map(t => t.tag),
377380
links: {
@@ -427,7 +430,7 @@ export class GraphSearchTool {
427430
const cache = this.app.metadataCache.getFileCache(file);
428431
nodes.push({
429432
path: edge.target,
430-
title: file.name.replace(/\.md$/, ''),
433+
title: this.graphTraversal.getNodeTitle(file),
431434
type: 'file',
432435
tags: cache?.tags?.map(t => t.tag),
433436
links: {
@@ -464,4 +467,4 @@ export class GraphSearchTool {
464467
}
465468
};
466469
}
467-
}
470+
}

src/utils/graph-traversal.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,31 @@ export interface GraphTraversalResult {
5555
export class GraphTraversal {
5656
constructor(private app: App) {}
5757

58+
/**
59+
* Get a human-friendly graph node title for a file.
60+
*
61+
* Vaults commonly use <topic>/index.md as landing pages. Showing every
62+
* such node as "index" makes graph results ambiguous, so use the parent
63+
* folder name for index files when available.
64+
*/
65+
getNodeTitle(file: TFile): string {
66+
if (file.basename === 'index' && file.parent?.name) {
67+
return file.parent.name;
68+
}
69+
return file.basename;
70+
}
71+
72+
/**
73+
* Resolve a path to a graph node title, falling back to the path basename.
74+
*/
75+
getNodeTitleForPath(filePath: string): string {
76+
const file = this.app.vault.getAbstractFileByPath(filePath);
77+
if (file instanceof TFile) {
78+
return this.getNodeTitle(file);
79+
}
80+
return filePath.replace(/\.md$/, '').split('/').pop() || filePath;
81+
}
82+
5883
/**
5984
* Get all nodes (files) in the vault
6085
*/
@@ -63,7 +88,7 @@ export class GraphTraversal {
6388
return files.map(file => ({
6489
file,
6590
path: file.path,
66-
title: file.basename,
91+
title: this.getNodeTitle(file),
6792
metadata: this.app.metadataCache.getFileCache(file) || undefined
6893
}));
6994
}
@@ -223,7 +248,7 @@ export class GraphTraversal {
223248
const node: GraphNode = {
224249
file,
225250
path: file.path,
226-
title: file.basename,
251+
title: this.getNodeTitle(file),
227252
metadata: this.app.metadataCache.getFileCache(file) || undefined
228253
};
229254

@@ -378,7 +403,7 @@ export class GraphTraversal {
378403
const neighbors: GraphNode[] = recentFiles.map(file => ({
379404
file,
380405
path: file.path,
381-
title: file.basename,
406+
title: this.getNodeTitle(file),
382407
metadata: this.app.metadataCache.getFileCache(file) || undefined
383408
}));
384409

@@ -413,7 +438,7 @@ export class GraphTraversal {
413438
const node: GraphNode = {
414439
file,
415440
path: file.path,
416-
title: file.basename,
441+
title: this.getNodeTitle(file),
417442
metadata: this.app.metadataCache.getFileCache(file) || undefined
418443
};
419444

@@ -432,7 +457,7 @@ export class GraphTraversal {
432457
neighbors.push({
433458
file: neighborFile,
434459
path: neighborFile.path,
435-
title: neighborFile.basename,
460+
title: this.getNodeTitle(neighborFile),
436461
metadata: this.app.metadataCache.getFileCache(neighborFile) || undefined
437462
});
438463
}
@@ -470,4 +495,4 @@ export class GraphTraversal {
470495
tagCount
471496
};
472497
}
473-
}
498+
}

tests/graph-node-title.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { App, TFile } from 'obsidian';
2+
import { GraphSearchTool } from '../src/tools/graph-search';
3+
import { GraphTraversal } from '../src/utils/graph-traversal';
4+
import { ObsidianAPI } from '../src/utils/obsidian-api';
5+
6+
function makeFile(path: string, basename: string, parentName?: string): TFile {
7+
const file = new TFile();
8+
file.path = path;
9+
file.name = `${basename}.md`;
10+
file.basename = basename;
11+
file.extension = 'md';
12+
(file as any).parent = parentName
13+
? { name: parentName }
14+
: null;
15+
return file;
16+
}
17+
18+
describe('graph node titles', () => {
19+
let app: App;
20+
let traversal: GraphTraversal;
21+
let search: GraphSearchTool;
22+
let indexFile: TFile;
23+
let regularFile: TFile;
24+
let sourceFile: TFile;
25+
26+
beforeEach(() => {
27+
indexFile = makeFile('topics/rendering/index.md', 'index', 'rendering');
28+
regularFile = makeFile('topics/rendering/overview.md', 'overview', 'rendering');
29+
sourceFile = makeFile('source.md', 'source');
30+
31+
const filesByPath = new Map([
32+
[indexFile.path, indexFile],
33+
[regularFile.path, regularFile],
34+
[sourceFile.path, sourceFile]
35+
]);
36+
37+
app = new App();
38+
(app as any).metadataCache = {
39+
resolvedLinks: {
40+
'source.md': { 'topics/rendering/index.md': 1 },
41+
'topics/rendering/index.md': { 'topics/rendering/overview.md': 1 }
42+
},
43+
unresolvedLinks: {},
44+
getFileCache: jest.fn().mockReturnValue({ tags: [] })
45+
};
46+
app.vault.getFiles = jest.fn(() => [indexFile, regularFile, sourceFile]);
47+
app.vault.getAbstractFileByPath = jest.fn((path: string) => filesByPath.get(path) ?? null);
48+
49+
traversal = new GraphTraversal(app);
50+
search = new GraphSearchTool({} as ObsidianAPI, app);
51+
});
52+
53+
it('uses the parent folder as the graph title for index files', () => {
54+
expect(traversal.getNodeTitle(indexFile)).toBe('rendering');
55+
expect(traversal.getNodeTitle(regularFile)).toBe('overview');
56+
});
57+
58+
it('uses resolved graph titles in vault node listings', () => {
59+
const nodes = traversal.getAllNodes();
60+
61+
expect(nodes).toEqual(
62+
expect.arrayContaining([
63+
expect.objectContaining({ path: 'topics/rendering/index.md', title: 'rendering' }),
64+
expect.objectContaining({ path: 'topics/rendering/overview.md', title: 'overview' })
65+
])
66+
);
67+
});
68+
69+
it('uses resolved graph titles in forwardlink results', () => {
70+
const result = search.search({ operation: 'forwardlinks', sourcePath: 'source.md' });
71+
72+
expect(result.nodes).toEqual([
73+
expect.objectContaining({ path: 'topics/rendering/index.md', title: 'rendering' })
74+
]);
75+
});
76+
77+
it('uses resolved graph titles in backlink results', () => {
78+
const result = search.search({ operation: 'backlinks', sourcePath: 'topics/rendering/overview.md' });
79+
80+
expect(result.nodes).toEqual([
81+
expect.objectContaining({ path: 'topics/rendering/index.md', title: 'rendering' })
82+
]);
83+
});
84+
85+
it('uses resolved graph titles in path results', () => {
86+
const result = search.search({
87+
operation: 'path',
88+
sourcePath: 'source.md',
89+
targetPath: 'topics/rendering/overview.md'
90+
});
91+
92+
expect(result.paths?.[0]).toEqual([
93+
{ path: 'source.md', title: 'source' },
94+
{ path: 'topics/rendering/index.md', title: 'rendering' },
95+
{ path: 'topics/rendering/overview.md', title: 'overview' }
96+
]);
97+
});
98+
});

0 commit comments

Comments
 (0)