Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
184 changes: 115 additions & 69 deletions src/linkManager.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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'] });
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -363,7 +370,7 @@ export class LinkManager {
color: color,
legendText: textL,
legendGraphics: graphicsL,
nUsing: 0,
nUsing: 1,
};

this.tagColors.set(linkString, newLegendGraphic);
Expand Down Expand Up @@ -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) {
Expand All @@ -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<any>).some(item => this.valueContainsTarget(item, sourceId, targetId));
}

if (this.isDataviewLink(value)) {
const candidates = new Set<string>();
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<string>();

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.
Expand Down
17 changes: 12 additions & 5 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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() {
Expand Down
9 changes: 0 additions & 9 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down