mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
fix: tags parsing issue in frontmatter
This commit is contained in:
parent
509245172b
commit
7d4fdc73ca
5 changed files with 340 additions and 21 deletions
|
|
@ -343,6 +343,125 @@ describe("File Metadata Inheritance", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("Tags Inheritance", () => {
|
||||
test("should inherit tags from file metadata", () => {
|
||||
const content = "- [ ] Task without tags";
|
||||
const fileMetadata = {
|
||||
tags: ["#work", "#urgent", "#meeting"],
|
||||
};
|
||||
|
||||
const tasks = parser.parseLegacy(content, "test.md", fileMetadata);
|
||||
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].metadata.tags).toBeDefined();
|
||||
expect(tasks[0].metadata.tags).toEqual(["#work", "#urgent", "#meeting"]);
|
||||
});
|
||||
|
||||
test("should merge task tags with inherited tags", () => {
|
||||
const content = "- [ ] Task with existing tags #personal";
|
||||
const fileMetadata = {
|
||||
tags: ["#work", "#urgent"],
|
||||
};
|
||||
|
||||
const tasks = parser.parseLegacy(content, "test.md", fileMetadata);
|
||||
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].metadata.tags).toBeDefined();
|
||||
expect(tasks[0].metadata.tags).toContain("#personal");
|
||||
expect(tasks[0].metadata.tags).toContain("#work");
|
||||
expect(tasks[0].metadata.tags).toContain("#urgent");
|
||||
});
|
||||
|
||||
test("should not duplicate tags when merging", () => {
|
||||
const content = "- [ ] Task with duplicate tag #work";
|
||||
const fileMetadata = {
|
||||
tags: ["#work", "#urgent"],
|
||||
};
|
||||
|
||||
const tasks = parser.parseLegacy(content, "test.md", fileMetadata);
|
||||
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].metadata.tags).toBeDefined();
|
||||
// Should only have one instance of #work
|
||||
const workTags = tasks[0].metadata.tags.filter((tag: string) => tag === "#work");
|
||||
expect(workTags).toHaveLength(1);
|
||||
expect(tasks[0].metadata.tags).toContain("#urgent");
|
||||
});
|
||||
|
||||
test("should parse special tag formats from file metadata", () => {
|
||||
const content = "- [ ] Task inheriting project tag";
|
||||
const fileMetadata = {
|
||||
tags: ["#project/myproject", "#area/work", "#@/office"],
|
||||
};
|
||||
|
||||
const tasks = parser.parseLegacy(content, "test.md", fileMetadata);
|
||||
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].metadata.project).toBe("myproject");
|
||||
expect(tasks[0].metadata.area).toBe("work");
|
||||
expect(tasks[0].metadata.context).toBe("office");
|
||||
expect(tasks[0].metadata.tags).toContain("#project/myproject");
|
||||
expect(tasks[0].metadata.tags).toContain("#area/work");
|
||||
expect(tasks[0].metadata.tags).toContain("#@/office");
|
||||
});
|
||||
|
||||
test("should prioritize task metadata over tag-derived metadata", () => {
|
||||
const content = "- [ ] Task with explicit project [project::taskproject]";
|
||||
const fileMetadata = {
|
||||
tags: ["#project/fileproject"],
|
||||
};
|
||||
|
||||
const tasks = parser.parseLegacy(content, "test.md", fileMetadata);
|
||||
|
||||
expect(tasks).toHaveLength(1);
|
||||
// Task's explicit project should take precedence
|
||||
expect(tasks[0].metadata.project).toBe("taskproject");
|
||||
expect(tasks[0].metadata.tags).toContain("#project/fileproject");
|
||||
});
|
||||
|
||||
test("should handle mixed tag formats in file metadata", () => {
|
||||
const content = "- [ ] Task with mixed tag inheritance";
|
||||
const fileMetadata = {
|
||||
tags: ["#regular-tag", "#project/myproject", "#normalTag", "#area/work"],
|
||||
};
|
||||
|
||||
const tasks = parser.parseLegacy(content, "test.md", fileMetadata);
|
||||
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].metadata.project).toBe("myproject");
|
||||
expect(tasks[0].metadata.area).toBe("work");
|
||||
expect(tasks[0].metadata.tags).toContain("#regular-tag");
|
||||
expect(tasks[0].metadata.tags).toContain("#normalTag");
|
||||
expect(tasks[0].metadata.tags).toContain("#project/myproject");
|
||||
expect(tasks[0].metadata.tags).toContain("#area/work");
|
||||
});
|
||||
|
||||
test("should handle empty tags array in file metadata", () => {
|
||||
const content = "- [ ] Task with empty tags";
|
||||
const fileMetadata = {
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const tasks = parser.parseLegacy(content, "test.md", fileMetadata);
|
||||
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].metadata.tags).toEqual([]);
|
||||
});
|
||||
|
||||
test("should handle non-array tags in file metadata", () => {
|
||||
const content = "- [ ] Task with non-array tags";
|
||||
const fileMetadata = {
|
||||
tags: "single-tag",
|
||||
};
|
||||
|
||||
const tasks = parser.parseLegacy(content, "test.md", fileMetadata);
|
||||
|
||||
expect(tasks).toHaveLength(1);
|
||||
// Should inherit as a single tag
|
||||
expect(tasks[0].metadata.tags).toContain("single-tag");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Configuration Migration", () => {
|
||||
test("should work with migrated settings", () => {
|
||||
// 模拟迁移后的设置结构
|
||||
|
|
|
|||
67
src/__tests__/TagsInheritanceInvestigation.test.ts
Normal file
67
src/__tests__/TagsInheritanceInvestigation.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* Investigation: Tags Inheritance and Metadata Fields
|
||||
*/
|
||||
|
||||
import { MarkdownTaskParser } from "../utils/workers/ConfigurableTaskParser";
|
||||
import { getConfig } from "../common/task-parser-config";
|
||||
import { createMockPlugin } from "./mockUtils";
|
||||
import { DEFAULT_SETTINGS } from "../common/setting-definition";
|
||||
|
||||
describe("Tags Inheritance Investigation", () => {
|
||||
test("should investigate what fields are being inherited incorrectly", () => {
|
||||
const mockPlugin = createMockPlugin({
|
||||
...DEFAULT_SETTINGS,
|
||||
fileMetadataInheritance: {
|
||||
enabled: true,
|
||||
inheritFromFrontmatter: true,
|
||||
inheritFromFrontmatterForSubtasks: false,
|
||||
},
|
||||
});
|
||||
|
||||
const config = getConfig("tasks", mockPlugin);
|
||||
const parser = new MarkdownTaskParser(config);
|
||||
|
||||
const content = `- [>] 12312312
|
||||
- [ ] child task`;
|
||||
|
||||
// Simulate problematic file metadata that might contain structural fields
|
||||
const fileMetadata = {
|
||||
tags: ["mobility"],
|
||||
children: ["some-other-task"], // This should NOT be inherited
|
||||
parent: "some-parent", // This should NOT be inherited
|
||||
heading: ["Some Heading"], // This should NOT be inherited
|
||||
id: "file-id", // This should NOT be inherited
|
||||
priority: "high", // This SHOULD be inherited
|
||||
area: "work", // This SHOULD be inherited
|
||||
};
|
||||
|
||||
const tasks = parser.parseLegacy(content, "templify-asdasdasd-20250704120358.md", fileMetadata);
|
||||
|
||||
const parentTask = tasks[0];
|
||||
const childTask = tasks[1];
|
||||
|
||||
// Check parent task
|
||||
console.log("Parent task metadata:", JSON.stringify(parentTask.metadata, null, 2));
|
||||
|
||||
// Check child task
|
||||
console.log("Child task metadata:", JSON.stringify(childTask.metadata, null, 2));
|
||||
|
||||
// The problematic fields should NOT be inherited from file metadata
|
||||
expect(parentTask.metadata.children).not.toEqual(["some-other-task"]);
|
||||
expect(parentTask.metadata.parent).not.toBe("some-parent");
|
||||
expect(parentTask.metadata.id).not.toBe("file-id");
|
||||
|
||||
// But the appropriate fields should be inherited
|
||||
expect(parentTask.metadata.priority).toBe(4); // "high" converted to 4
|
||||
expect(parentTask.metadata.area).toBe("work");
|
||||
expect(parentTask.metadata.tags).toContain("mobility");
|
||||
|
||||
// Parent task should have correct structural fields
|
||||
expect(parentTask.metadata.children).toEqual([childTask.id]);
|
||||
expect(parentTask.metadata.parent).toBeUndefined();
|
||||
|
||||
// Child task should have correct structural fields
|
||||
expect(childTask.metadata.children).toEqual([]);
|
||||
expect(childTask.metadata.parent).toBe(parentTask.id);
|
||||
});
|
||||
});
|
||||
|
|
@ -5,6 +5,11 @@
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-genius-view:has(.task-details.visible) .tags-left-column {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tags-content {
|
||||
|
|
|
|||
|
|
@ -128,6 +128,24 @@ export class MarkdownTaskParser {
|
|||
isSubtask
|
||||
);
|
||||
|
||||
// Process inherited tags and merge with task's own tags
|
||||
let finalTags = tags;
|
||||
if (inheritedMetadata.tags) {
|
||||
try {
|
||||
const inheritedTags = JSON.parse(
|
||||
inheritedMetadata.tags
|
||||
);
|
||||
if (Array.isArray(inheritedTags)) {
|
||||
finalTags = this.mergeTags(tags, inheritedTags);
|
||||
}
|
||||
} catch (e) {
|
||||
// If parsing fails, treat as a single tag
|
||||
finalTags = this.mergeTags(tags, [
|
||||
inheritedMetadata.tags,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Use provided tgProject or determine from config
|
||||
const taskTgProject =
|
||||
tgProject || this.determineTgProject(filePath);
|
||||
|
|
@ -154,7 +172,7 @@ export class MarkdownTaskParser {
|
|||
parentId,
|
||||
childrenIds: [],
|
||||
metadata: inheritedMetadata,
|
||||
tags,
|
||||
tags: finalTags,
|
||||
comment,
|
||||
lineNumber: i + 1,
|
||||
actualIndent: actualSpaces,
|
||||
|
|
@ -1096,6 +1114,17 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
private convertToLegacyTask(enhancedTask: EnhancedTask): Task {
|
||||
// Helper function to safely parse tags from metadata
|
||||
const parseTagsFromMetadata = (tagsString: string): string[] => {
|
||||
try {
|
||||
const parsed = JSON.parse(tagsString);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (e) {
|
||||
// If parsing fails, treat as a single tag
|
||||
return [tagsString];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
id: enhancedTask.id,
|
||||
content: enhancedTask.content,
|
||||
|
|
@ -1106,8 +1135,11 @@ export class MarkdownTaskParser {
|
|||
originalMarkdown: enhancedTask.originalMarkdown,
|
||||
children: enhancedTask.children || [],
|
||||
metadata: {
|
||||
tags: enhancedTask.tags || enhancedTask.metadata.tags,
|
||||
children: enhancedTask.children,
|
||||
tags:
|
||||
enhancedTask.tags ||
|
||||
(enhancedTask.metadata.tags
|
||||
? parseTagsFromMetadata(enhancedTask.metadata.tags)
|
||||
: []),
|
||||
priority:
|
||||
enhancedTask.priority || enhancedTask.metadata.priority,
|
||||
startDate:
|
||||
|
|
@ -1136,6 +1168,8 @@ export class MarkdownTaskParser {
|
|||
.filter((id) => id.length > 0)
|
||||
: undefined,
|
||||
onCompletion: enhancedTask.metadata.onCompletion,
|
||||
// Legacy compatibility fields that should remain in metadata
|
||||
children: enhancedTask.children,
|
||||
heading: Array.isArray(enhancedTask.heading)
|
||||
? enhancedTask.heading
|
||||
: enhancedTask.heading
|
||||
|
|
@ -1238,6 +1272,55 @@ export class MarkdownTaskParser {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tags array to extract special tag formats and convert them to metadata
|
||||
* @param tags Array of tags to parse
|
||||
* @returns Object containing extracted metadata from tags
|
||||
*/
|
||||
private parseTagsForMetadata(tags: string[]): Record<string, string> {
|
||||
const metadata: Record<string, string> = {};
|
||||
|
||||
for (const tag of tags) {
|
||||
// Remove # prefix if present
|
||||
const tagWithoutHash = tag.startsWith("#") ? tag.substring(1) : tag;
|
||||
const slashPos = tagWithoutHash.indexOf("/");
|
||||
|
||||
if (slashPos !== -1) {
|
||||
const prefix = tagWithoutHash.substring(0, slashPos);
|
||||
const value = tagWithoutHash.substring(slashPos + 1);
|
||||
|
||||
// Check if this is a special tag prefix that should be converted to metadata
|
||||
const metadataKey = this.config.specialTagPrefixes[prefix];
|
||||
if (
|
||||
metadataKey &&
|
||||
this.config.metadataParseMode !== MetadataParseMode.None
|
||||
) {
|
||||
metadata[metadataKey] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge tags from different sources, removing duplicates
|
||||
* @param baseTags Base tags array (from task)
|
||||
* @param inheritedTags Tags to inherit (from file metadata)
|
||||
* @returns Merged tags array with duplicates removed
|
||||
*/
|
||||
private mergeTags(baseTags: string[], inheritedTags: string[]): string[] {
|
||||
const merged = [...baseTags];
|
||||
|
||||
for (const tag of inheritedTags) {
|
||||
if (!merged.includes(tag)) {
|
||||
merged.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherit metadata from file frontmatter and project configuration
|
||||
*/
|
||||
|
|
@ -1336,28 +1419,69 @@ export class MarkdownTaskParser {
|
|||
"indentLevel",
|
||||
"actualIndent",
|
||||
"listMarker",
|
||||
"tgProject",
|
||||
"comment",
|
||||
"metadata", // Prevent recursive metadata inheritance
|
||||
]);
|
||||
|
||||
// Inherit from file metadata (frontmatter) if available
|
||||
if (this.fileMetadata) {
|
||||
for (const [key, value] of Object.entries(this.fileMetadata)) {
|
||||
// Only inherit if:
|
||||
// 1. The field is not in the non-inheritable list
|
||||
// 2. The task doesn't already have a meaningful value for this field
|
||||
// 3. The file metadata value is not undefined/null
|
||||
if (
|
||||
!nonInheritableFields.has(key) &&
|
||||
(inherited[key] === undefined ||
|
||||
inherited[key] === null ||
|
||||
inherited[key] === "") &&
|
||||
value !== undefined &&
|
||||
value !== null
|
||||
) {
|
||||
// Convert priority values to numbers before inheritance
|
||||
if (key === "priority") {
|
||||
inherited[key] = convertPriorityValue(value);
|
||||
} else {
|
||||
inherited[key] = String(value);
|
||||
// Special handling for tags field
|
||||
if (key === "tags" && Array.isArray(value)) {
|
||||
// Parse tags to extract special tag formats (e.g., #project/myproject)
|
||||
const tagMetadata = this.parseTagsForMetadata(value);
|
||||
|
||||
// Merge extracted metadata from tags
|
||||
for (const [tagKey, tagValue] of Object.entries(
|
||||
tagMetadata
|
||||
)) {
|
||||
if (
|
||||
!nonInheritableFields.has(tagKey) &&
|
||||
(inherited[tagKey] === undefined ||
|
||||
inherited[tagKey] === null ||
|
||||
inherited[tagKey] === "") &&
|
||||
tagValue !== undefined &&
|
||||
tagValue !== null
|
||||
) {
|
||||
// Convert priority values to numbers before inheritance
|
||||
if (tagKey === "priority") {
|
||||
inherited[tagKey] =
|
||||
convertPriorityValue(tagValue);
|
||||
} else {
|
||||
inherited[tagKey] = String(tagValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the tags array itself as tags metadata
|
||||
if (
|
||||
!nonInheritableFields.has("tags") &&
|
||||
(inherited["tags"] === undefined ||
|
||||
inherited["tags"] === null ||
|
||||
inherited["tags"] === "")
|
||||
) {
|
||||
inherited["tags"] = JSON.stringify(value);
|
||||
}
|
||||
} else {
|
||||
// Only inherit if:
|
||||
// 1. The field is not in the non-inheritable list
|
||||
// 2. The task doesn't already have a meaningful value for this field
|
||||
// 3. The file metadata value is not undefined/null
|
||||
if (
|
||||
!nonInheritableFields.has(key) &&
|
||||
(inherited[key] === undefined ||
|
||||
inherited[key] === null ||
|
||||
inherited[key] === "") &&
|
||||
value !== undefined &&
|
||||
value !== null
|
||||
) {
|
||||
// Convert priority values to numbers before inheritance
|
||||
if (key === "priority") {
|
||||
inherited[key] = convertPriorityValue(value);
|
||||
} else {
|
||||
inherited[key] = String(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue