refactor: link-utils to use adapters

This commit is contained in:
2025-10-20 07:55:30 -04:00
parent 45f4184b08
commit 360f4269f2

View File

@@ -1,4 +1,5 @@
import { App, TFile, MetadataCache } from 'obsidian'; import { TFile } from 'obsidian';
import { IVaultAdapter, IMetadataCacheAdapter } from '../adapters/interfaces';
/** /**
* Parsed wikilink structure * Parsed wikilink structure
@@ -114,14 +115,15 @@ export class LinkUtils {
* Resolve a wikilink to its target file * Resolve a wikilink to its target file
* Uses Obsidian's MetadataCache for accurate resolution * Uses Obsidian's MetadataCache for accurate resolution
* *
* @param app Obsidian App instance * @param vault Vault adapter for file operations
* @param metadata Metadata cache adapter for link resolution
* @param sourcePath Path of the file containing the link * @param sourcePath Path of the file containing the link
* @param linkText Link text (without brackets) * @param linkText Link text (without brackets)
* @returns Resolved file or null if not found * @returns Resolved file or null if not found
*/ */
static resolveLink(app: App, sourcePath: string, linkText: string): TFile | null { static resolveLink(vault: IVaultAdapter, metadata: IMetadataCacheAdapter, sourcePath: string, linkText: string): TFile | null {
// Get the source file // Get the source file
const sourceFile = app.vault.getAbstractFileByPath(sourcePath); const sourceFile = vault.getAbstractFileByPath(sourcePath);
if (!(sourceFile instanceof TFile)) { if (!(sourceFile instanceof TFile)) {
return null; return null;
} }
@@ -132,7 +134,7 @@ export class LinkUtils {
// - Relative paths // - Relative paths
// - Aliases // - Aliases
// - Headings and blocks // - Headings and blocks
const resolvedFile = app.metadataCache.getFirstLinkpathDest(linkText, sourcePath); const resolvedFile = metadata.getFirstLinkpathDest(linkText, sourcePath);
return resolvedFile; return resolvedFile;
} }
@@ -141,13 +143,13 @@ export class LinkUtils {
* Find potential matches for an unresolved link * Find potential matches for an unresolved link
* Uses fuzzy matching on file names * Uses fuzzy matching on file names
* *
* @param app Obsidian App instance * @param vault Vault adapter for file operations
* @param linkText Link text to find matches for * @param linkText Link text to find matches for
* @param maxSuggestions Maximum number of suggestions to return * @param maxSuggestions Maximum number of suggestions to return
* @returns Array of suggested file paths * @returns Array of suggested file paths
*/ */
static findSuggestions(app: App, linkText: string, maxSuggestions: number = 5): string[] { static findSuggestions(vault: IVaultAdapter, linkText: string, maxSuggestions: number = 5): string[] {
const allFiles = app.vault.getMarkdownFiles(); const allFiles = vault.getMarkdownFiles();
const suggestions: Array<{ path: string; score: number }> = []; const suggestions: Array<{ path: string; score: number }> = [];
// Remove heading/block references for matching // Remove heading/block references for matching
@@ -197,18 +199,20 @@ export class LinkUtils {
* Get all backlinks to a file * Get all backlinks to a file
* Uses Obsidian's MetadataCache for accurate backlink detection * Uses Obsidian's MetadataCache for accurate backlink detection
* *
* @param app Obsidian App instance * @param vault Vault adapter for file operations
* @param metadata Metadata cache adapter for link resolution
* @param targetPath Path of the file to find backlinks for * @param targetPath Path of the file to find backlinks for
* @param includeUnlinked Whether to include unlinked mentions * @param includeUnlinked Whether to include unlinked mentions
* @returns Array of backlinks * @returns Array of backlinks
*/ */
static async getBacklinks( static async getBacklinks(
app: App, vault: IVaultAdapter,
metadata: IMetadataCacheAdapter,
targetPath: string, targetPath: string,
includeUnlinked: boolean = false includeUnlinked: boolean = false
): Promise<Backlink[]> { ): Promise<Backlink[]> {
const backlinks: Backlink[] = []; const backlinks: Backlink[] = [];
const targetFile = app.vault.getAbstractFileByPath(targetPath); const targetFile = vault.getAbstractFileByPath(targetPath);
if (!(targetFile instanceof TFile)) { if (!(targetFile instanceof TFile)) {
return backlinks; return backlinks;
@@ -219,7 +223,7 @@ export class LinkUtils {
// Get all backlinks from MetadataCache using resolvedLinks // Get all backlinks from MetadataCache using resolvedLinks
// resolvedLinks is a map of: sourcePath -> { targetPath: linkCount } // resolvedLinks is a map of: sourcePath -> { targetPath: linkCount }
const resolvedLinks = app.metadataCache.resolvedLinks; const resolvedLinks = metadata.resolvedLinks;
// Find all files that link to our target // Find all files that link to our target
for (const [sourcePath, links] of Object.entries(resolvedLinks)) { for (const [sourcePath, links] of Object.entries(resolvedLinks)) {
@@ -228,13 +232,13 @@ export class LinkUtils {
continue; continue;
} }
const sourceFile = app.vault.getAbstractFileByPath(sourcePath); const sourceFile = vault.getAbstractFileByPath(sourcePath);
if (!(sourceFile instanceof TFile)) { if (!(sourceFile instanceof TFile)) {
continue; continue;
} }
// Read the source file to find link occurrences // Read the source file to find link occurrences
const content = await app.vault.read(sourceFile); const content = await vault.read(sourceFile);
const lines = content.split('\n'); const lines = content.split('\n');
const occurrences: BacklinkOccurrence[] = []; const occurrences: BacklinkOccurrence[] = [];
@@ -243,7 +247,7 @@ export class LinkUtils {
for (const link of wikilinks) { for (const link of wikilinks) {
// Resolve this link to see if it points to our target // Resolve this link to see if it points to our target
const resolvedFile = this.resolveLink(app, sourcePath, link.target); const resolvedFile = this.resolveLink(vault, metadata, sourcePath, link.target);
if (resolvedFile && resolvedFile.path === targetPath) { if (resolvedFile && resolvedFile.path === targetPath) {
const snippet = this.extractSnippet(lines, link.line - 1, 100); const snippet = this.extractSnippet(lines, link.line - 1, 100);
@@ -265,7 +269,7 @@ export class LinkUtils {
// Process unlinked mentions if requested // Process unlinked mentions if requested
if (includeUnlinked) { if (includeUnlinked) {
const allFiles = app.vault.getMarkdownFiles(); const allFiles = vault.getMarkdownFiles();
// Build a set of files that already have linked backlinks // Build a set of files that already have linked backlinks
const linkedSourcePaths = new Set(backlinks.map(b => b.sourcePath)); const linkedSourcePaths = new Set(backlinks.map(b => b.sourcePath));
@@ -281,7 +285,7 @@ export class LinkUtils {
continue; continue;
} }
const content = await app.vault.read(file); const content = await vault.read(file);
const lines = content.split('\n'); const lines = content.split('\n');
const occurrences: BacklinkOccurrence[] = []; const occurrences: BacklinkOccurrence[] = [];
@@ -345,30 +349,32 @@ export class LinkUtils {
/** /**
* Validate all wikilinks in a file * Validate all wikilinks in a file
* @param app Obsidian App instance * @param vault Vault adapter for file operations
* @param metadata Metadata cache adapter for link resolution
* @param filePath Path of the file to validate * @param filePath Path of the file to validate
* @returns Object with resolved and unresolved links * @returns Object with resolved and unresolved links
*/ */
static async validateWikilinks( static async validateWikilinks(
app: App, vault: IVaultAdapter,
metadata: IMetadataCacheAdapter,
filePath: string filePath: string
): Promise<{ ): Promise<{
resolvedLinks: ResolvedLink[]; resolvedLinks: ResolvedLink[];
unresolvedLinks: UnresolvedLink[]; unresolvedLinks: UnresolvedLink[];
}> { }> {
const file = app.vault.getAbstractFileByPath(filePath); const file = vault.getAbstractFileByPath(filePath);
if (!(file instanceof TFile)) { if (!(file instanceof TFile)) {
return { resolvedLinks: [], unresolvedLinks: [] }; return { resolvedLinks: [], unresolvedLinks: [] };
} }
const content = await app.vault.read(file); const content = await vault.read(file);
const wikilinks = this.parseWikilinks(content); const wikilinks = this.parseWikilinks(content);
const resolvedLinks: ResolvedLink[] = []; const resolvedLinks: ResolvedLink[] = [];
const unresolvedLinks: UnresolvedLink[] = []; const unresolvedLinks: UnresolvedLink[] = [];
for (const link of wikilinks) { for (const link of wikilinks) {
const resolvedFile = this.resolveLink(app, filePath, link.target); const resolvedFile = this.resolveLink(vault, metadata, filePath, link.target);
if (resolvedFile) { if (resolvedFile) {
resolvedLinks.push({ resolvedLinks.push({
@@ -377,7 +383,7 @@ export class LinkUtils {
alias: link.alias alias: link.alias
}); });
} else { } else {
const suggestions = this.findSuggestions(app, link.target); const suggestions = this.findSuggestions(vault, link.target);
unresolvedLinks.push({ unresolvedLinks.push({
text: link.raw, text: link.raw,
line: link.line, line: link.line,