import type { Project, ProjectConfig, StatusConfig, Task } from '../types' import { isTerminalStatus, sanitizeFileName } from '../utils' import { appendYaml, FRONTMATTER_KEY, stripAutoGeneratedContent, TASK_FRONTMATTER_KEY } from './YamlParser' /** Only fields the project actually overrides; an empty block is omitted entirely. */ function serializeProjectConfig(config: ProjectConfig | undefined): Record | null { if (!config) return null const out: Record = {} if (config.statuses?.length) out.statuses = config.statuses if (config.priorities?.length) out.priorities = config.priorities if (config.defaultView) out.defaultView = config.defaultView if (config.autoSchedule !== undefined) out.autoSchedule = config.autoSchedule if (config.pullForwardOnEarlyFinish !== undefined) out.pullForwardOnEarlyFinish = config.pullForwardOnEarlyFinish if (config.kanbanShowSubtasks !== undefined) out.kanbanShowSubtasks = config.kanbanShowSubtasks if (config.kanbanShowDescriptionPreview !== undefined) { out.kanbanShowDescriptionPreview = config.kanbanShowDescriptionPreview } return Object.keys(out).length ? out : null } export function serializeProject(project: Project, statuses: StatusConfig[] = []): string { const seen = new Set() const tasks: Task[] = [] for (const t of project.tasks) { if (seen.has(t.id)) continue seen.add(t.id) tasks.push(t) } const taskIds = tasks.map((t) => t.id) const fm: Record = { [FRONTMATTER_KEY]: true, id: project.id, title: project.title, description: project.description, color: project.color, icon: project.icon, taskIds, customFields: project.customFields, teamMembers: project.teamMembers, savedViews: project.savedViews.length ? project.savedViews : [], createdAt: project.createdAt, updatedAt: project.updatedAt } const config = serializeProjectConfig(project.config) if (config) fm.config = config const yamlLines: string[] = ['---'] appendYaml(yamlLines, fm, 0) yamlLines.push('---') yamlLines.push('') yamlLines.push(`# ${project.icon} ${project.title}`) yamlLines.push('') if (project.description) { yamlLines.push(project.description) yamlLines.push('') } if (tasks.length) { yamlLines.push('## Tasks') for (const t of tasks) { if (t.filePath) { const basename = t.filePath.replace(/^.*\//, '').replace(/\.md$/, '') const check = isTerminalStatus(t.status, statuses) ? 'x' : ' ' yamlLines.push(`- [${check}] [[${basename}|${t.title}]]`) } } yamlLines.push('') } return yamlLines.join('\n') } export function buildTaskFrontmatter(task: Task, project: Project, parentTask: Task | null): Record { const fm: Record = { [TASK_FRONTMATTER_KEY]: true, projectId: project.id, parentId: parentTask?.id ?? null, id: task.id, title: task.title, type: task.type, status: task.status, priority: task.priority, start: task.start, due: task.due, progress: task.progress, assignees: task.assignees, tags: task.tags, subtaskIds: task.subtasks.map((s) => s.id), dependencies: task.dependencies, createdAt: task.createdAt, updatedAt: task.updatedAt } if (task.completed) fm.completed = task.completed if (task.recurrence) fm.recurrence = task.recurrence if (task.timeEstimate !== undefined) fm.timeEstimate = task.timeEstimate if (task.timeLogs?.length) fm.timeLogs = task.timeLogs if (Object.keys(task.customFields).length) fm.customFields = task.customFields return fm } export function serializeTask( task: Task, project: Project, parentTask: Task | null, statuses: StatusConfig[] = [] ): string { const fm = buildTaskFrontmatter(task, project, parentTask) const yamlLines: string[] = ['---'] appendYaml(yamlLines, fm, 0) yamlLines.push('---') yamlLines.push('') const cleanDesc = stripAutoGeneratedContent(task.description) if (cleanDesc) { yamlLines.push(cleanDesc) yamlLines.push('') } if (parentTask?.filePath) { const parentBasename = parentTask.filePath.replace(/^.*\//, '').replace(/\.md$/, '') yamlLines.push(`Parent: [[${parentBasename}|${parentTask.title}]]`) } else { const projectBasename = project.filePath.replace(/^.*\//, '').replace(/\.md$/, '') yamlLines.push(`Project: [[${projectBasename}|${project.title}]]`) } if (task.subtasks.length) { yamlLines.push('') yamlLines.push('## Subtasks') for (const sub of task.subtasks) { const subBasename = sub.filePath ? sub.filePath.replace(/^.*\//, '').replace(/\.md$/, '') : taskSlug(sub.title) const check = isTerminalStatus(sub.status, statuses) ? 'x' : ' ' yamlLines.push(`- [${check}] [[${subBasename}|${sub.title}]]`) } } return yamlLines.join('\n') } /** Keeps paths under Windows MAX_PATH and the limits mobile and sync impose. */ export const TASK_SLUG_MAX_LENGTH = 60 function taskSlug(title: string): string { return sanitizeFileName(title).toLowerCase().replace(/\s+/g, '-').slice(0, TASK_SLUG_MAX_LENGTH) } export function taskFilePath(taskTitle: string, folder: string): string { return `${folder}/${taskSlug(taskTitle)}.md` }