fix: tags parsing issue in frontmatter

This commit is contained in:
quorafind 2025-07-15 16:23:31 +08:00
parent 509245172b
commit 7d4fdc73ca
5 changed files with 340 additions and 21 deletions

View file

@ -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", () => {
// 模拟迁移后的设置结构

View 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);
});
});

View file

@ -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 {

View file

@ -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