mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
refactor: improve link handling with Obsidian API
Replace manual wikilink generation and parsing with Obsidian's native APIs throughout the codebase for better compatibility and correctness. Changes: - Create linkUtils.ts with centralized link generation helpers - Use FileManager.generateMarkdownLink() for all link creation - Respects user's link format settings (wikilink vs markdown) - Handles relative paths according to user preferences - Future-proof against Obsidian link format changes - Use parseLinktext() for proper wikilink parsing - Correctly handles [[link|alias]] syntax - Eliminates manual regex patterns Files updated: - TaskContextMenu: project references and subtask creation - TaskModal/TaskEditModal: project and subtask list rendering - dependencyUtils: link formatting and resolution - ProjectNoteDecorations: new subtask creation - TaskCard: project comparison with alias support - StatsView: wikilink path extraction Benefits: - Better compatibility with user settings - Proper alias handling throughout - Less code to maintain - More robust link parsing
This commit is contained in:
parent
0329945061
commit
918297a2f6
8 changed files with 113 additions and 29 deletions
|
|
@ -15,6 +15,7 @@ import {
|
|||
formatDependencyLink,
|
||||
normalizeDependencyEntry,
|
||||
} from "../utils/dependencyUtils";
|
||||
import { generateLink } from "../utils/linkUtils";
|
||||
|
||||
export interface TaskContextMenuOptions {
|
||||
task: TaskInfo;
|
||||
|
|
@ -572,7 +573,7 @@ export class TaskContextMenu {
|
|||
item.onClick(() => {
|
||||
const taskFile = plugin.app.vault.getAbstractFileByPath(task.path);
|
||||
if (taskFile instanceof TFile) {
|
||||
const projectReference = `[[${taskFile.basename}]]`;
|
||||
const projectReference = generateLink(plugin.app, taskFile, task.path);
|
||||
plugin.openTaskCreationModal({
|
||||
projects: [projectReference],
|
||||
});
|
||||
|
|
@ -904,7 +905,7 @@ export class TaskContextMenu {
|
|||
return;
|
||||
}
|
||||
|
||||
const projectReference = this.buildProjectReference(projectFile, task.path, plugin);
|
||||
const projectReference = generateLink(plugin.app, projectFile, task.path);
|
||||
const legacyReference = `[[${projectFile.basename}]]`;
|
||||
const currentProjects = Array.isArray(task.projects) ? task.projects : [];
|
||||
|
||||
|
|
@ -939,7 +940,7 @@ export class TaskContextMenu {
|
|||
return;
|
||||
}
|
||||
|
||||
const projectReference = this.buildProjectReference(currentTaskFile, subtask.path, plugin);
|
||||
const projectReference = generateLink(plugin.app, currentTaskFile, subtask.path);
|
||||
const legacyReference = `[[${currentTaskFile.basename}]]`;
|
||||
const subtaskProjects = Array.isArray(subtask.projects) ? subtask.projects : [];
|
||||
|
||||
|
|
@ -968,8 +969,7 @@ export class TaskContextMenu {
|
|||
}
|
||||
|
||||
private buildProjectReference(targetFile: TFile, sourcePath: string, plugin: TaskNotesPlugin): string {
|
||||
const linkText = plugin.app.metadataCache.fileToLinktext(targetFile, sourcePath, true);
|
||||
return `[[${linkText}]]`;
|
||||
return generateLink(plugin.app, targetFile, sourcePath);
|
||||
}
|
||||
|
||||
private updateMainMenuIconColors(task: TaskInfo, plugin: TaskNotesPlugin): void {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { GroupingUtils } from "../utils/GroupingUtils";
|
|||
import { ProjectSubtasksService } from "../services/ProjectSubtasksService";
|
||||
import TaskNotesPlugin from "../main";
|
||||
import { createTaskCard } from "../ui/TaskCard";
|
||||
import { generateLink } from "../utils/linkUtils";
|
||||
|
||||
// Define a state effect for project subtasks updates
|
||||
const projectSubtasksUpdateEffect = StateEffect.define<{ forceUpdate?: boolean }>();
|
||||
|
|
@ -691,8 +692,8 @@ export class ProjectSubtasksWidget extends WidgetType {
|
|||
return;
|
||||
}
|
||||
|
||||
// Create wikilink format for the project reference
|
||||
const projectReference = `[[${currentFile.basename}]]`;
|
||||
// Create link using Obsidian's API for proper format
|
||||
const projectReference = generateLink(this.plugin.app, currentFile, currentFile.path);
|
||||
|
||||
// Open task creation modal with project pre-populated
|
||||
this.plugin.openTaskCreationModal({
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from "../utils/helpers";
|
||||
import { splitListPreservingLinksAndQuotes } from "../utils/stringSplit";
|
||||
import { ReminderContextMenu } from "../components/ReminderContextMenu";
|
||||
import { generateLinkWithDisplay } from "../utils/linkUtils";
|
||||
|
||||
export interface TaskEditOptions {
|
||||
task: TaskInfo;
|
||||
|
|
@ -953,11 +954,13 @@ export class TaskEditModal extends TaskModal {
|
|||
}
|
||||
|
||||
this.selectedProjectFiles.forEach((file) => {
|
||||
if (!(file instanceof TFile)) return;
|
||||
|
||||
const projectItem = this.projectsList.createDiv({ cls: "task-project-item" });
|
||||
const infoEl = projectItem.createDiv({ cls: "task-project-info" });
|
||||
const nameEl = infoEl.createDiv({ cls: "task-project-name clickable-project" });
|
||||
|
||||
const projectAsWikilink = `[[${file.path}|${file.name}]]`;
|
||||
const projectAsWikilink = generateLinkWithDisplay(this.app, file, this.task.path, file.name);
|
||||
this.renderProjectLinksWithoutPrefix(nameEl, [projectAsWikilink]);
|
||||
|
||||
if (file.path !== file.name) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
type LinkServices,
|
||||
} from "../ui/renderers/linkRenderer";
|
||||
import { TaskSelectorModal } from "./TaskSelectorModal";
|
||||
import { generateLink, generateLinkWithDisplay } from "../utils/linkUtils";
|
||||
|
||||
interface DependencyItem {
|
||||
dependency: TaskDependency;
|
||||
|
|
@ -98,14 +99,19 @@ export abstract class TaskModal extends Modal {
|
|||
};
|
||||
}
|
||||
|
||||
// For unresolved dependencies, create a minimal file-like object to generate link
|
||||
const basename = path.split("/").pop() || path;
|
||||
const nameWithoutExt = basename.replace(/\.md$/i, "");
|
||||
|
||||
// Use a simple wikilink format for unresolved dependencies
|
||||
// This will be resolved when the file is created
|
||||
return {
|
||||
dependency: {
|
||||
uid: `[[${basename.replace(/\.md$/i, "")}]]`,
|
||||
uid: `[[${nameWithoutExt}]]`,
|
||||
reltype: DEFAULT_DEPENDENCY_RELTYPE,
|
||||
},
|
||||
path,
|
||||
name: basename.replace(/\.md$/i, ""),
|
||||
name: nameWithoutExt,
|
||||
unresolved: true,
|
||||
};
|
||||
}
|
||||
|
|
@ -1378,8 +1384,7 @@ export abstract class TaskModal extends Modal {
|
|||
}
|
||||
|
||||
protected buildProjectReference(targetFile: TFile, sourcePath: string): string {
|
||||
const linkText = this.app.metadataCache.fileToLinktext(targetFile, sourcePath, true);
|
||||
return `[[${linkText}]]`;
|
||||
return generateLink(this.app, targetFile, sourcePath);
|
||||
}
|
||||
|
||||
protected initializeProjectsFromStrings(projects: string[]): void {
|
||||
|
|
@ -1431,11 +1436,13 @@ export abstract class TaskModal extends Modal {
|
|||
}
|
||||
|
||||
this.selectedProjectFiles.forEach((file) => {
|
||||
if (!(file instanceof TFile)) return;
|
||||
|
||||
const projectItem = this.projectsList.createDiv({ cls: "task-project-item" });
|
||||
const infoEl = projectItem.createDiv({ cls: "task-project-info" });
|
||||
const nameEl = infoEl.createDiv({ cls: "task-project-name clickable-project" });
|
||||
|
||||
const projectAsWikilink = `[[${file.path}|${file.name}]]`;
|
||||
const projectAsWikilink = generateLinkWithDisplay(this.app, file, this.getCurrentTaskPath() || "", file.name);
|
||||
this.renderProjectLinksWithoutPrefix(nameEl, [projectAsWikilink]);
|
||||
|
||||
if (file.path !== file.name) {
|
||||
|
|
@ -1515,11 +1522,13 @@ export abstract class TaskModal extends Modal {
|
|||
}
|
||||
|
||||
this.selectedSubtaskFiles.forEach((file) => {
|
||||
if (!(file instanceof TFile)) return;
|
||||
|
||||
const subtaskItem = this.subtasksList.createDiv({ cls: "task-project-item" });
|
||||
const infoEl = subtaskItem.createDiv({ cls: "task-project-info" });
|
||||
const nameEl = infoEl.createDiv({ cls: "task-project-name clickable-project" });
|
||||
|
||||
const taskLink = `[[${file.path}|${file.name}]]`;
|
||||
const taskLink = generateLinkWithDisplay(this.app, file, this.getCurrentTaskPath() || "", file.name);
|
||||
this.renderProjectLinksWithoutPrefix(nameEl, [taskLink]);
|
||||
|
||||
if (file.path !== file.name) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable no-console */
|
||||
import { TFile, setIcon, Notice, Modal, App, setTooltip } from "obsidian";
|
||||
import { TFile, setIcon, Notice, Modal, App, setTooltip, parseLinktext } from "obsidian";
|
||||
import { TaskInfo } from "../types";
|
||||
import TaskNotesPlugin from "../main";
|
||||
import { TaskContextMenu } from "../components/TaskContextMenu";
|
||||
|
|
@ -2300,7 +2300,8 @@ export async function refreshParentTaskSubtasks(
|
|||
project.startsWith("[[") &&
|
||||
project.endsWith("]]")
|
||||
) {
|
||||
const linkedNoteName = project.slice(2, -2).trim();
|
||||
const linkContent = project.slice(2, -2).trim();
|
||||
const linkedNoteName = parseLinktext(linkContent).path;
|
||||
// Check both exact match and resolved file match
|
||||
const resolvedFile = plugin.app.metadataCache.getFirstLinkpathDest(
|
||||
linkedNoteName,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { App, TFile } from "obsidian";
|
||||
import { App, TFile, parseLinktext } from "obsidian";
|
||||
import type { TaskDependency, TaskDependencyRelType } from "../types";
|
||||
import { splitListPreservingLinksAndQuotes } from "./stringSplit";
|
||||
import { generateLink } from "./linkUtils";
|
||||
|
||||
export const DEFAULT_DEPENDENCY_RELTYPE: TaskDependencyRelType = "FINISHTOSTART";
|
||||
|
||||
|
|
@ -127,11 +128,12 @@ export function resolveDependencyEntry(
|
|||
return null;
|
||||
}
|
||||
|
||||
// Use Obsidian's parseLinktext for proper wikilink parsing
|
||||
// This handles both [[link]] and [[link|alias]] formats correctly
|
||||
let target = trimmed;
|
||||
if (trimmed.startsWith("[[") && trimmed.endsWith("]]")) {
|
||||
const inner = trimmed.slice(2, -2).trim();
|
||||
const pipeIndex = inner.indexOf("|");
|
||||
target = pipeIndex >= 0 ? inner.substring(0, pipeIndex).trim() : inner;
|
||||
target = parseLinktext(inner).path;
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
|
|
@ -154,10 +156,10 @@ export function resolveDependencyEntry(
|
|||
export function formatDependencyLink(app: App, sourcePath: string, targetPath: string): string {
|
||||
const target = app.vault.getAbstractFileByPath(targetPath);
|
||||
if (target instanceof TFile) {
|
||||
const linktext = app.metadataCache.fileToLinktext(target, sourcePath, false);
|
||||
return `[[${linktext}]]`;
|
||||
return generateLink(app, target, sourcePath);
|
||||
}
|
||||
|
||||
// For unresolved files, create a simple wikilink with the basename
|
||||
const basename = targetPath.split("/").pop() || targetPath;
|
||||
return `[[${basename.replace(/\.md$/i, "")}]]`;
|
||||
}
|
||||
|
|
|
|||
73
src/utils/linkUtils.ts
Normal file
73
src/utils/linkUtils.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { App, TFile } from "obsidian";
|
||||
|
||||
/**
|
||||
* Generate a properly formatted markdown link using Obsidian's API.
|
||||
* This respects user's link format settings (wikilink vs markdown, relative paths, etc.)
|
||||
*
|
||||
* @param app - Obsidian app instance
|
||||
* @param targetFile - The file to link to
|
||||
* @param sourcePath - The path of the file containing the link (for relative paths)
|
||||
* @param subpath - Optional subpath (e.g., heading anchor)
|
||||
* @param alias - Optional display alias
|
||||
* @returns A properly formatted link string
|
||||
*/
|
||||
export function generateLink(
|
||||
app: App,
|
||||
targetFile: TFile,
|
||||
sourcePath: string,
|
||||
subpath?: string,
|
||||
alias?: string
|
||||
): string {
|
||||
return app.fileManager.generateMarkdownLink(
|
||||
targetFile,
|
||||
sourcePath,
|
||||
subpath || "",
|
||||
alias || ""
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a link with the file's basename as the alias.
|
||||
* Useful for creating links that display the file name.
|
||||
*
|
||||
* @param app - Obsidian app instance
|
||||
* @param targetFile - The file to link to
|
||||
* @param sourcePath - The path of the file containing the link
|
||||
* @returns A link with basename as alias
|
||||
*/
|
||||
export function generateLinkWithBasename(
|
||||
app: App,
|
||||
targetFile: TFile,
|
||||
sourcePath: string
|
||||
): string {
|
||||
return app.fileManager.generateMarkdownLink(
|
||||
targetFile,
|
||||
sourcePath,
|
||||
"",
|
||||
targetFile.basename
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a link with a custom path display as the alias.
|
||||
* Useful for showing full paths in UI.
|
||||
*
|
||||
* @param app - Obsidian app instance
|
||||
* @param targetFile - The file to link to
|
||||
* @param sourcePath - The path of the file containing the link
|
||||
* @param displayName - Custom display name
|
||||
* @returns A link with custom display name as alias
|
||||
*/
|
||||
export function generateLinkWithDisplay(
|
||||
app: App,
|
||||
targetFile: TFile,
|
||||
sourcePath: string,
|
||||
displayName: string
|
||||
): string {
|
||||
return app.fileManager.generateMarkdownLink(
|
||||
targetFile,
|
||||
sourcePath,
|
||||
"",
|
||||
displayName
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { ItemView, WorkspaceLeaf, Setting, EventRef } from "obsidian";
|
||||
import { ItemView, WorkspaceLeaf, Setting, EventRef, parseLinktext } from "obsidian";
|
||||
import {
|
||||
format,
|
||||
startOfWeek,
|
||||
|
|
@ -462,13 +462,8 @@ export class StatsView extends ItemView {
|
|||
|
||||
const linkContent = projectValue.slice(2, -2);
|
||||
|
||||
// Handle alias syntax: [[path|alias]]
|
||||
const pipeIndex = linkContent.indexOf("|");
|
||||
if (pipeIndex !== -1) {
|
||||
return linkContent.substring(0, pipeIndex).trim();
|
||||
}
|
||||
|
||||
return linkContent;
|
||||
// Use Obsidian's parseLinktext to handle aliases properly
|
||||
return parseLinktext(linkContent).path;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue