mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
fix(write): preserve wiki links and inline code when updating task metadata
- Improved metadata detection to avoid matching content inside wiki links [[...]], markdown links [text](url), and inline code blocks - Fixed issue where links in task content could be incorrectly treated as metadata markers - Added comprehensive test to verify links and code blocks are preserved during metadata updates - Ensures task content remains intact when only metadata is being modified
This commit is contained in:
parent
968e5dcc41
commit
a787cee793
2 changed files with 133 additions and 5 deletions
98
src/__tests__/writeapi-links-preserve.test.ts
Normal file
98
src/__tests__/writeapi-links-preserve.test.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { App, MetadataCache } from "obsidian";
|
||||
import { WriteAPI } from "@/dataflow/api/WriteAPI";
|
||||
import type { Task } from "@/types/task";
|
||||
|
||||
/**
|
||||
* Ensures updateTask preserves wiki/markdown links and inline code in task content
|
||||
* while replacing/rewriting trailing metadata.
|
||||
*/
|
||||
describe("WriteAPI.updateTask preserves links in content when regenerating metadata", () => {
|
||||
it("keeps [[wiki#section|alias]] and [text](url#anchor) and `code` intact and replaces metadata", async () => {
|
||||
// In-memory vault mock
|
||||
let fileContent =
|
||||
"- [ ] Do [[Page#Heading|alias]] and [text](https://ex.com/a#b) `inline` #oldtag 🔁 every day 🛫 2024-12-01";
|
||||
const filePath = "Test.md";
|
||||
|
||||
const fakeVault: any = {
|
||||
getAbstractFileByPath: (path: string) => ({ path }),
|
||||
read: async (_file: any) => fileContent,
|
||||
modify: async (_file: any, newContent: string) => {
|
||||
fileContent = newContent;
|
||||
},
|
||||
};
|
||||
|
||||
// Minimal app and metadataCache mocks
|
||||
const app = new App();
|
||||
const metadataCache = new MetadataCache();
|
||||
|
||||
// Ensure workspace has trigger/on for Events.emit compatibility
|
||||
(app as any).workspace = {
|
||||
...(app as any).workspace,
|
||||
trigger: jest.fn(),
|
||||
on: jest.fn(() => ({ unload: () => {} })),
|
||||
};
|
||||
|
||||
// Minimal plugin settings used by generateMetadata
|
||||
const plugin: any = {
|
||||
settings: {
|
||||
preferMetadataFormat: "tasks", // use emoji/tokens format
|
||||
projectTagPrefix: { tasks: "project", dataview: "project" },
|
||||
contextTagPrefix: { tasks: "@", dataview: "context" },
|
||||
taskStatuses: { completed: "x" },
|
||||
autoDateManager: {
|
||||
manageStartDate: false,
|
||||
manageCancelledDate: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// getTaskById returns a task pointing to line 0 in file
|
||||
const getTaskById = async (id: string): Promise<Task | null> => {
|
||||
if (id !== "1") return null;
|
||||
return {
|
||||
id: "1",
|
||||
content:
|
||||
"Do [[Page#Heading|alias]] and [text](https://ex.com/a#b) `inline`",
|
||||
filePath,
|
||||
line: 0,
|
||||
completed: false,
|
||||
status: " ",
|
||||
originalMarkdown: fileContent,
|
||||
metadata: {
|
||||
tags: ["oldtag"],
|
||||
children: [],
|
||||
} as any,
|
||||
} as Task;
|
||||
};
|
||||
|
||||
const writeAPI = new WriteAPI(
|
||||
app as any,
|
||||
fakeVault,
|
||||
metadataCache as any,
|
||||
plugin,
|
||||
getTaskById
|
||||
);
|
||||
|
||||
// Act: update metadata only (do not touch content/status)
|
||||
const due = new Date("2025-01-15").valueOf();
|
||||
const res = await writeAPI.updateTask({
|
||||
taskId: "1",
|
||||
updates: {
|
||||
metadata: { tags: ["newtag"], dueDate: due } as any,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.success).toBe(true);
|
||||
|
||||
// Assert: links and inline code remain; old metadata removed; new metadata appended
|
||||
// Expect tags first then due date (emoji 📅) in tasks format
|
||||
expect(fileContent).toContain(
|
||||
"- [ ] Do [[Page#Heading|alias]] and [text](https://ex.com/a#b) `inline` #newtag 📅 2025-01-15"
|
||||
);
|
||||
|
||||
// Ensure no remnants of old metadata tokens
|
||||
expect(fileContent).not.toMatch(
|
||||
/#oldtag|🔁\s+every day|🛫\s+2024-12-01/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -482,11 +482,41 @@ export class WriteAPI {
|
|||
}
|
||||
} else {
|
||||
// Remove existing metadata and regenerate from merged values
|
||||
const prefixMatch = taskLine.match(
|
||||
/^(\s*[-*+]\s*\[[^\]]*\]\s*[^🔺⏫🔼🔽⏬🛫⏳📅✅🔁\[#@+]*)/
|
||||
// First, extract the checkbox prefix
|
||||
const checkboxMatch = taskLine.match(
|
||||
/^(\s*[-*+]\s*\[[^\]]*\]\s*)/
|
||||
);
|
||||
if (prefixMatch) {
|
||||
const taskPrefix = prefixMatch[0];
|
||||
if (checkboxMatch) {
|
||||
const checkboxPrefix = checkboxMatch[1];
|
||||
const afterCheckbox = taskLine.substring(
|
||||
checkboxPrefix.length
|
||||
);
|
||||
|
||||
// Find where metadata starts (look for emoji markers or dataview fields)
|
||||
// Updated pattern to avoid matching wiki links [[...]] or markdown links [text](url)
|
||||
// To avoid false positives, sanitize out wiki links [[...]], markdown links [text](url), and inline code `...`
|
||||
const sanitized = afterCheckbox
|
||||
// Use non-whitespace placeholders to prevent \s+ from consuming across links/code
|
||||
.replace(/\[\[[^\]]*\]\]/g, (m) =>
|
||||
"x".repeat(m.length)
|
||||
)
|
||||
.replace(/\[[^\]]*\]\([^\)]*\)/g, (m) =>
|
||||
"x".repeat(m.length)
|
||||
)
|
||||
.replace(/`[^`]*`/g, (m) => "x".repeat(m.length));
|
||||
|
||||
const metadataMatch = sanitized.match(
|
||||
/([\s]+(🔺|⏫|🔼|🔽|⏬|🛫|⏳|📅|✅|🔁|\[(?!\[\])[^\[\]\r\n:]+::|#[A-Za-z][\w/-]*|@[A-Za-z][\w/-]*|\+[A-Za-z][\w/-]*).*)?$/
|
||||
);
|
||||
|
||||
// Extract the task content (everything before metadata)
|
||||
const taskContent =
|
||||
metadataMatch && metadataMatch.index !== undefined
|
||||
? afterCheckbox
|
||||
.substring(0, metadataMatch.index)
|
||||
.trim()
|
||||
: afterCheckbox.trim();
|
||||
|
||||
const mergedMd = {
|
||||
...originalTask.metadata,
|
||||
...args.updates.metadata,
|
||||
|
|
@ -510,7 +540,7 @@ export class WriteAPI {
|
|||
dependsOn: mergedMd.dependsOn,
|
||||
id: mergedMd.id,
|
||||
});
|
||||
taskLine = `${taskPrefix}${
|
||||
taskLine = `${checkboxPrefix}${taskContent}${
|
||||
newMetadata ? ` ${newMetadata}` : ""
|
||||
}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue