Skip to content

Commit 59f3d3e

Browse files
committed
Use plain text Google Calendar descriptions
1 parent 24de7e6 commit 59f3d3e

3 files changed

Lines changed: 75 additions & 7 deletions

File tree

docs/releases/unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ Example:
3030
- Adds a minimal display duration before passing point-in-time external events to FullCalendar
3131
- Preserves the original provider event data for context menus and debugging
3232
- Thanks to @martin-forge for reporting and debugging
33+
- Google Calendar task descriptions now use mobile-friendly plain text for Obsidian links and display labels for wiki-style project/context links.

src/services/TaskCalendarSyncService.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,12 +289,12 @@ export class TaskCalendarSyncService {
289289

290290
// Add contexts
291291
if (task.contexts && task.contexts.length > 0) {
292-
parts.push(t("contexts", { value: task.contexts.map((c) => `@${c}`).join(", ") }));
292+
parts.push(t("contexts", { value: task.contexts.map((c) => `@${this.toCalendarDescriptionLabel(c)}`).join(", ") }));
293293
}
294294

295295
// Add projects
296296
if (task.projects && task.projects.length > 0) {
297-
parts.push(t("projects", { value: task.projects.join(", ") }));
297+
parts.push(t("projects", { value: task.projects.map((p) => this.toCalendarDescriptionLabel(p)).join(", ") }));
298298
}
299299

300300
// Add separator before link
@@ -308,14 +308,28 @@ export class TaskCalendarSyncService {
308308
const vaultName = this.plugin.app.vault.getName();
309309
const encodedPath = encodeURIComponent(task.path);
310310
const obsidianUri = `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodedPath}`;
311-
// Google Calendar renders HTML in descriptions, so use an anchor tag
312311
const linkText = t("openInObsidian");
313-
parts.push(`<a href="${obsidianUri}">${linkText}</a>`);
312+
parts.push(`${linkText}: ${obsidianUri}`);
314313
}
315314

316315
return parts.join("\n");
317316
}
318317

318+
private toCalendarDescriptionLabel(value: string): string {
319+
return value
320+
.replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, "$2")
321+
.replace(/\[\[([^\]]+)\]\]/g, (_match, target: string) => this.basenameForDisplay(target))
322+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1")
323+
.trim();
324+
}
325+
326+
private basenameForDisplay(target: string): string {
327+
const withoutHeading = target.split("#")[0];
328+
const withoutExtension = withoutHeading.replace(/\.md$/i, "");
329+
const basename = withoutExtension.split("/").pop();
330+
return basename || withoutExtension || target;
331+
}
332+
319333
/**
320334
* Get the date to use for the calendar event based on settings
321335
*/

tests/services/TaskCalendarSyncService.test.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,38 @@ describe("TaskCalendarSyncService", () => {
1414
googleCalendarExport: {
1515
syncOnTaskUpdate: true,
1616
targetCalendarId: "test-calendar",
17+
includeObsidianLink: true,
1718
}
1819
},
20+
app: {
21+
vault: {
22+
getName: jest.fn().mockReturnValue("Martin OS"),
23+
},
24+
},
1925
cacheManager: {
2026
getTaskInfo: jest.fn()
2127
},
2228
statusManager: {
23-
getStatusConfig: jest.fn().mockReturnValue({ label: "Todo" })
29+
getStatusConfig: jest.fn((status: string) => ({ label: status === "ready" ? "Ready" : "Todo" }))
2430
},
2531
priorityManager: {
26-
getPriorityConfig: jest.fn().mockReturnValue({ label: "High" })
32+
getPriorityConfig: jest.fn((priority: string) => ({ label: priority === "2-high" ? "High" : "Medium" }))
2733
},
2834
i18n: {
29-
translate: jest.fn().mockReturnValue("Untitled Task")
35+
translate: jest.fn((key: string, params?: Record<string, string | number>) => {
36+
const translations: Record<string, string> = {
37+
"settings.integrations.googleCalendarExport.eventDescription.untitledTask": "Untitled Task",
38+
"settings.integrations.googleCalendarExport.eventDescription.priority": "Priority: {value}",
39+
"settings.integrations.googleCalendarExport.eventDescription.status": "Status: {value}",
40+
"settings.integrations.googleCalendarExport.eventDescription.scheduled": "Scheduled: {value}",
41+
"settings.integrations.googleCalendarExport.eventDescription.timeEstimate": "Time Estimate: {value}",
42+
"settings.integrations.googleCalendarExport.eventDescription.contexts": "Contexts: {value}",
43+
"settings.integrations.googleCalendarExport.eventDescription.projects": "Projects: {value}",
44+
"settings.integrations.googleCalendarExport.eventDescription.openInObsidian": "Open in Obsidian",
45+
};
46+
const translation = translations[key] || key;
47+
return translation.replace(/\{(\w+)\}/g, (_match, name) => String(params?.[name] ?? ""));
48+
})
3049
}
3150
};
3251

@@ -78,4 +97,38 @@ describe("TaskCalendarSyncService", () => {
7897
expect(syncService.executeTaskUpdate).toHaveBeenCalledTimes(1);
7998
expect(syncService.executeTaskUpdate).toHaveBeenCalledWith(secondPayload);
8099
});
100+
101+
it("should build plain-text calendar descriptions for external calendar clients", () => {
102+
const description = syncService.buildEventDescription({
103+
path: "1 Tasks/Tasks/Download first personal data export batch.md",
104+
title: "Download first personal data export batch",
105+
status: "ready",
106+
priority: "2-high",
107+
scheduled: "2026-04-29",
108+
timeEstimate: 180,
109+
projects: [
110+
"[[0 Collect personal data exports for vault intelligence|Collect personal data exports for vault intelligence]]",
111+
"[[Projects/Nested Project.md]]",
112+
"[Markdown Project](Projects/Markdown%20Project.md)",
113+
],
114+
contexts: ["[[People/Martin Ball|Martin Ball]]", "admin"],
115+
} as TaskInfo);
116+
117+
expect(description).toContain("Priority: High");
118+
expect(description).toContain("Status: Ready");
119+
expect(description).toContain("Scheduled: 2026-04-29");
120+
expect(description).toContain("Time Estimate: 3h 0m");
121+
expect(description).toContain("Contexts: @Martin Ball, @admin");
122+
expect(description).toContain(
123+
"Projects: Collect personal data exports for vault intelligence, Nested Project, Markdown Project"
124+
);
125+
expect(description).toContain(
126+
"Open in Obsidian: obsidian://open?vault=Martin%20OS&file=1%20Tasks%2FTasks%2FDownload%20first%20personal%20data%20export%20batch.md"
127+
);
128+
expect(description).not.toContain("[[");
129+
expect(description).not.toContain("]]");
130+
expect(description).not.toContain("<a ");
131+
expect(description).not.toContain("</a>");
132+
expect(description).not.toContain("](");
133+
});
81134
});

0 commit comments

Comments
 (0)