From 7eeb80dae7cb6c2b061fb052c0f2f5a4216b8411 Mon Sep 17 00:00:00 2001 From: Kidan Nelson <44580969+natefrisch01@users.noreply.github.com> Date: Sat, 16 May 2026 08:09:47 -0400 Subject: [PATCH] Fix link parsing and graph refresh bugs --- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/linkManager.ts | 184 ++++++++++++++++++++++++++++----------------- src/main.ts | 17 +++-- src/types.ts | 9 --- 6 files changed, 131 insertions(+), 87 deletions(-) diff --git a/manifest.json b/manifest.json index 07b2a30..b42a69a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "graph-link-types", "name": "Graph Link Types", - "version": "0.3.4", + "version": "0.4.0", "minAppVersion": "1.5.0", "description": "Link types for graph view.", "author": "natefrisch01", diff --git a/package-lock.json b/package-lock.json index 665c35f..0e25d0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "graph-link-types", - "version": "0.3.3", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "graph-link-types", - "version": "0.3.3", + "version": "0.4.0", "license": "MIT", "dependencies": { "markdown-link-extractor": "^3.1.0", diff --git a/package.json b/package.json index 42297a7..3aeb269 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graph-link-types", - "version": "0.3.3", + "version": "0.4.0", "description": "Link types for Obsidian graph view.", "main": "main.js", "scripts": { diff --git a/src/linkManager.ts b/src/linkManager.ts index 7ca65d6..b39cb50 100644 --- a/src/linkManager.ts +++ b/src/linkManager.ts @@ -1,5 +1,6 @@ -import { ObsidianRenderer, ObsidianLink, LinkPair, GltLink, DataviewLinkType , GltLegendGraphic} from 'src/types'; +import { getLinkpath, normalizePath } from 'obsidian'; +import { ObsidianRenderer, ObsidianLink, LinkPair, GltLink, GltLegendGraphic} from 'src/types'; import { Text, TextStyle , Graphics, Color} from 'pixi.js'; // @ts-ignore @@ -60,20 +61,22 @@ export class LinkManager { let lastTheme = ''; let lastStyleSheetHref = ''; let debounceTimer: number; + + const updateThemeColors = () => { + this.currentTheme = document.body.classList.contains('theme-dark') ? 'theme-dark' : 'theme-light'; + const currentStyleSheetHref = document.querySelector('link[rel="stylesheet"][href*="theme"]')?.getAttribute('href') ?? ''; + if ((this.currentTheme && this.currentTheme !== lastTheme) || (currentStyleSheetHref !== lastStyleSheetHref)) { + this.textColor = this.getComputedColorFromClass(this.currentTheme, '--text-normal'); + lastTheme = this.currentTheme; + lastStyleSheetHref = currentStyleSheetHref; + } + }; + + updateThemeColors(); const themeObserver = new MutationObserver(() => { clearTimeout(debounceTimer); - debounceTimer = window.setTimeout(() => { - this.currentTheme = document.body.classList.contains('theme-dark') ? 'theme-dark' : 'theme-light'; - const currentStyleSheetHref = document.querySelector('link[rel="stylesheet"][href*="theme"]')?.getAttribute('href'); - if ((this.currentTheme && this.currentTheme !== lastTheme) || (currentStyleSheetHref !== lastStyleSheetHref)) { - this.textColor = this.getComputedColorFromClass(this.currentTheme, '--text-normal'); - lastTheme = this.currentTheme; - if (currentStyleSheetHref) { - lastStyleSheetHref = currentStyleSheetHref; - } - } - }, 100); // Debounce delay + debounceTimer = window.setTimeout(updateThemeColors, 100); }); themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] }); @@ -176,7 +179,11 @@ export class LinkManager { } removeLinks(renderer: ObsidianRenderer, currentLinks: ObsidianLink[]): void { - const currentKeys = new Set(currentLinks.map(link => this.generateKey(link.source.id, link.target.id))); + const currentKeys = new Set( + currentLinks + .filter((link): link is ObsidianLink => Boolean(link?.source?.id && link?.target?.id)) + .map(link => this.generateKey(link.source.id, link.target.id)) + ); // remove any links in our map that aren't in this list this.linksMap.forEach((_, key) => { if (!currentKeys.has(key)) { @@ -363,7 +370,7 @@ export class LinkManager { color: color, legendText: textL, legendGraphics: graphicsL, - nUsing: 0, + nUsing: 1, }; this.tagColors.set(linkString, newLegendGraphic); @@ -392,28 +399,6 @@ export class LinkManager { return graphics } - // Utility function to extract the file path from a Markdown link - private extractPathFromMarkdownLink(markdownLink: string | unknown): string { - const links = extractLinks(markdownLink).links; - // The package returns an array of links. Assuming you want the first link. - return links.length > 0 ? links[0] : ''; - } - - // Method to determine the type of a value, now a class method - private determineDataviewLinkType(value: any): DataviewLinkType { - if (typeof value === 'object' && value !== null && 'path' in value) { - return DataviewLinkType.WikiLink; - } else if (typeof value === 'string' && value.includes('](')) { - return DataviewLinkType.MarkdownLink; - } else if (typeof value === 'string') { - return DataviewLinkType.String; - } else if (Array.isArray(value)) { - return DataviewLinkType.Array; - } else { - return DataviewLinkType.Other; - } - } - // Remove all text nodes from the graph destroyMap(renderer: ObsidianRenderer): void { if (this.linksMap.size > 0) { @@ -429,44 +414,105 @@ export class LinkManager { if (!sourcePage) return null; for (const [key, value] of Object.entries(sourcePage)) { - // Skip empty values - if (value === null || value === undefined || value === '') { - continue; - } - const valueType = this.determineDataviewLinkType(value); - - switch (valueType) { - case DataviewLinkType.WikiLink: - // @ts-ignore - if (value.path === targetId) { - return key; - } - break; - case DataviewLinkType.MarkdownLink: - if (this.extractPathFromMarkdownLink(value) === targetId) { - return key; - } - break; - case DataviewLinkType.Array: - // @ts-ignore - for (const item of value) { - if (this.determineDataviewLinkType(item) === DataviewLinkType.WikiLink && item.path === targetId) { - return key; - } - if (this.determineDataviewLinkType(item) === DataviewLinkType.MarkdownLink && this.extractPathFromMarkdownLink(item) === targetId) { - return key; - } - } - break; - default: - // We will continue to check other DataView properties - break; + if (value === null || value === undefined || value === '') { + continue; + } + + if (this.valueContainsTarget(value, sourceId, targetId)) { + return key; } } - // If no DataView properties match, we consider that metadata key does not exist + return null; } + private valueContainsTarget(value: any, sourceId: string, targetId: string): boolean { + if (this.isArrayLike(value)) { + return Array.from(value as Iterable).some(item => this.valueContainsTarget(item, sourceId, targetId)); + } + + if (this.isDataviewLink(value)) { + const candidates = new Set(); + candidates.add(value.path); + + if (typeof value.obsidianLink === 'function') { + candidates.add(value.obsidianLink()); + } + + if (typeof value.fileName === 'function') { + candidates.add(value.fileName()); + } + + return Array.from(candidates).some(candidate => this.linkPathMatchesTarget(candidate, sourceId, targetId)); + } + + if (typeof value === 'string') { + return this.extractLinkPathsFromString(value) + .some(candidate => this.linkPathMatchesTarget(candidate, sourceId, targetId)); + } + + return false; + } + + private isArrayLike(value: any): boolean { + return Array.isArray(value) || Boolean(this.api?.isArray?.(value)); + } + + private isDataviewLink(value: any): value is { + path: string; + obsidianLink?: () => string; + fileName?: () => string; + } { + return typeof value === 'object' + && value !== null + && typeof value.path === 'string'; + } + + private extractLinkPathsFromString(value: string): string[] { + const paths = new Set(); + + try { + for (const path of extractLinks(value).links) { + paths.add(path); + } + } catch { + // Keep checking wikilinks even if the markdown parser rejects this value. + } + + const wikiLinkPattern = /!?\[\[([^\]]+)\]\]/g; + let wikiLinkMatch: RegExpExecArray | null; + while ((wikiLinkMatch = wikiLinkPattern.exec(value)) !== null) { + paths.add(wikiLinkMatch[1]); + } + + return Array.from(paths); + } + + private linkPathMatchesTarget(rawLinkPath: string, sourceId: string, targetId: string): boolean { + const candidate = this.normalizeLinkPath(rawLinkPath); + const normalizedTarget = normalizePath(targetId); + + if (candidate === normalizedTarget || `${candidate}.md` === normalizedTarget) { + return true; + } + + const resolvedFile = this.api?.app?.metadataCache?.getFirstLinkpathDest(candidate, sourceId); + return resolvedFile?.path === normalizedTarget; + } + + private normalizeLinkPath(rawLinkPath: string): string { + const withoutAlias = rawLinkPath.split('|', 1)[0]; + let decoded = withoutAlias; + + try { + decoded = decodeURIComponent(withoutAlias); + } catch { + // Keep the original value if it contains malformed percent encoding. + } + + return normalizePath(getLinkpath(decoded.trim())); + } + // Function to calculate the coordinates for placing the link text. private getLinkToTextCoordinates(linkX: number, linkY: number, panX: number, panY: number, scale: number): { x: number, y: number } { // Apply scaling and panning to calculate the actual position. diff --git a/src/main.ts b/src/main.ts index e44370a..936a374 100644 --- a/src/main.ts +++ b/src/main.ts @@ -89,18 +89,17 @@ export default class GraphLinkTypesPlugin extends Plugin { this.registerEvent(this.app.metadataCache.on("dataview:api-ready", () => { this.api = getAPI(); this.linkManager.api = this.api; + this.indexReady = Boolean(this.api?.index?.initialized); this.initEventHandlers(); - // Only start rendering if a graph view is already open. - // Otherwise layout-change handler picks it up when one opens. - if (this.currentRenderer) { - this.startUpdateLoop(); - } + void this.handleLayoutChange(); })); return; } this.linkManager.api = this.api; + this.indexReady = Boolean(this.api.index?.initialized); this.initEventHandlers(); + await this.handleLayoutChange(); } private initEventHandlers(): void { @@ -120,6 +119,14 @@ export default class GraphLinkTypesPlugin extends Plugin { this.handleLayoutChange(); } })); + + this.registerEvent(this.app.metadataCache.on('resolved', () => { + this.handleLayoutChange(); + })); + + this.registerEvent(this.app.vault.on('rename', () => { + this.handleLayoutChange(); + })); } async loadSettings() { diff --git a/src/types.ts b/src/types.ts index fcc83da..4b8e3eb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,15 +37,6 @@ export interface ObsidianLink { }; } -// Define the enum outside the class -export enum DataviewLinkType { - WikiLink, - MarkdownLink, - String, - Array, - Other -} - // Define a numeric enum for link statuses export enum LinkPair { None,