- Introduced a wide two-column layout for the Chain Workbench modal, improving user experience and accessibility. - Added new styles for workbench components, including headers, filters, and main containers, to enhance visual organization. - Updated chain templates to allow for multiple distinct matches per template, improving flexibility in template matching. - Enhanced documentation to clarify the new settings and commands related to the Chain Workbench and edge detection features. - Implemented logging for better tracking of missing configurations, ensuring users are informed about any loading issues.
192 lines
5.7 KiB
TypeScript
192 lines
5.7 KiB
TypeScript
/**
|
|
* Helper functions for unresolved link detection and handling.
|
|
*/
|
|
|
|
import { App, TFile } from "obsidian";
|
|
|
|
/**
|
|
* Normalize heading string for comparison only (Inspect Chains / Chain Workbench).
|
|
* Maps "Überschrift", "Überschrift ^Block", "Überschrift Block" to the same canonical form
|
|
* so that Obsidian UI links (no ^) and plugin/sectionParser variants match.
|
|
* - Strip trailing block-id with caret: \s+\^[a-zA-Z0-9_-]+$
|
|
* - Strip one trailing word (Obsidian stores "Überschrift Block" without ^)
|
|
* Use only for equality checks, not for display or stored links.
|
|
*/
|
|
export function normalizeHeadingForMatch(heading: string | null): string | null {
|
|
if (heading === null || heading === undefined) return null;
|
|
let s = heading.trim();
|
|
if (!s) return null;
|
|
// 1) Remove optional block-id suffix with caret: "Überschrift ^Block" -> "Überschrift"
|
|
s = s.replace(/\s+\^[a-zA-Z0-9_-]+\s*$/, "").trim();
|
|
// 2) Remove one trailing word (Obsidian link form "Überschrift Block" -> "Überschrift")
|
|
s = s.replace(/\s+[a-zA-Z0-9_-]+\s*$/, "").trim();
|
|
return s || null;
|
|
}
|
|
|
|
/**
|
|
* Compare two headings for equality using normalized form (block-id suffix and trailing word stripped).
|
|
* Use for Inspect Chains / Chain Workbench matching only.
|
|
*/
|
|
export function headingsMatch(a: string | null, b: string | null): boolean {
|
|
const na = normalizeHeadingForMatch(a);
|
|
const nb = normalizeHeadingForMatch(b);
|
|
if (na === null && nb === null) return true;
|
|
if (na === null || nb === null) return false;
|
|
return na === nb;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
});
|
|
}
|