mindnet_obsidian/src/unresolvedLink/linkHelpers.ts

161 lines
4.3 KiB
TypeScript
Raw Normal View History

/**
* Helper functions for unresolved link detection and handling.
*/
import { App, TFile } from "obsidian";
/**
* Normalize link target by removing alias separator (|) and heading separator (#).
* Preserves spaces and case.
* Examples:
* - "file#sec|alias" -> "file"
* - "My Note" -> "My Note"
* - "My Note|Display" -> "My Note"
*/
export function normalizeLinkTarget(raw: string): string {
if (!raw) return "";
// Split by pipe (alias separator) and take first part
const parts = raw.split("|");
const firstPart = parts[0];
if (!firstPart) return raw.trim();
// Split by hash (heading separator) and take first part
const baseParts = firstPart.split("#");
const base = baseParts[0];
if (!base) return raw.trim();
return base.trim();
}
/**
* Extract link target from an anchor element.
* Prefers data-href attribute, falls back to textContent.
* Returns normalized target (without alias/heading).
*/
export function extractLinkTargetFromAnchor(anchor: HTMLElement): string | null {
const dataHref = anchor.getAttribute("data-href");
const textContent = anchor.textContent;
// Try data-href first
if (dataHref) {
return normalizeLinkTarget(dataHref);
}
// Fall back to text content
if (textContent) {
return normalizeLinkTarget(textContent.trim());
}
return null;
}
/**
* Check if a link target is unresolved (file doesn't exist).
* Uses metadataCache as single source of truth.
*/
export function isUnresolvedLink(
app: App,
target: string,
sourcePath: string
): boolean {
if (!target) return false;
const resolvedFile = app.metadataCache.getFirstLinkpathDest(target, sourcePath);
return resolvedFile === null;
}
/**
* Parse wikilink at cursor position in a line of text.
* Finds the nearest [[...]] that contains or is near the cursor position.
* Returns the raw target (with alias/heading) or null if not found.
*/
export function parseWikilinkAtPosition(
lineText: string,
cursorPosInLine: number
): string | null {
if (!lineText || cursorPosInLine < 0) return null;
// Find all [[...]] pairs in the line
const linkPattern = /\[\[([^\]]+)\]\]/g;
const matches: Array<{ start: number; end: number; target: string }> = [];
let match;
while ((match = linkPattern.exec(lineText)) !== null) {
const start = match.index;
const end = match.index + match[0].length;
const target = match[1]; // Content between [[ and ]]
if (target) {
matches.push({ start, end, target });
}
}
if (matches.length === 0) return null;
// Find the link that contains the cursor or is closest to it
for (const link of matches) {
if (cursorPosInLine >= link.start && cursorPosInLine <= link.end) {
// Cursor is inside this link
return link.target;
}
}
// If cursor is not inside any link, find the nearest one
let nearestLink: { start: number; end: number; target: string } | null = null;
let minDistance = Infinity;
for (const link of matches) {
// Distance to link start or end, whichever is closer
const distToStart = Math.abs(cursorPosInLine - link.start);
const distToEnd = Math.abs(cursorPosInLine - link.end);
const distance = Math.min(distToStart, distToEnd);
if (distance < minDistance) {
minDistance = distance;
nearestLink = link;
}
}
// Only return if reasonably close (e.g., within 10 characters)
if (nearestLink && minDistance <= 10) {
return nearestLink.target;
}
return null;
}
/**
* Wait for a file to be modified after creation, or timeout.
* Returns "modified" if modify event fired, "timeout" otherwise.
* Useful for Templater compatibility.
*/
export function waitForFileModify(
app: App,
file: TFile,
timeoutMs: number
): Promise<"modified" | "timeout"> {
return new Promise((resolve) => {
let resolved = false;
const timeout = setTimeout(() => {
if (!resolved) {
resolved = true;
app.vault.off("modify", handler);
resolve("timeout");
}
}, timeoutMs);
const handler = (modifiedFile: TFile) => {
if (modifiedFile.path === file.path && !resolved) {
resolved = true;
clearTimeout(timeout);
app.vault.off("modify", handler);
resolve("modified");
}
};
app.vault.on("modify", handler);
});
}