// Inverse of the parser — serializes TodoFile back to MIME TODO format import type { TodoFile } from "./file" import type { Issue } from "./issue" import type { Sprint } from "./sprint" import type { Module, BugzillaTracker } from "./tracker" // Word-wrap text to fit within maxCol, respecting prefix widths // Returns array of lines (without prefix/indent — caller adds those) export function wordWrap( text: string, maxCol: number, firstPrefixLen: number, contIndentLen: number ): string[] { const result: string[] = [] // Split on existing newlines first, then wrap each paragraph const paragraphs = text.split("\n") for (const para of paragraphs) { const words = para.split(/\s+/).filter(Boolean) if (words.length === 0) { result.push("") continue } let line = "" const width = result.length === 0 ? maxCol - firstPrefixLen : maxCol - contIndentLen for (const word of words) { const lineWidth = result.length === 0 && line === "" ? maxCol - firstPrefixLen : maxCol - contIndentLen if (line === "") { line = word } else if (line.length + 1 + word.length <= lineWidth) { line += " " + word } else { result.push(line) line = word } } if (line) result.push(line) } return result.length > 0 ? result : [""] } export function serializeRelationships(rels: Issue["relationships"]): string { const parts: string[] = [] for (const [kind, ids] of Object.entries(rels)) { if (ids?.length) { parts.push(`${kind}:${ids.join(" ")}`) } } return parts.join(", ") } export function serializeSprints(sprints: Sprint[]): string { if (sprints.length === 0) return "Sprints:" const lines = ["Sprints:"] for (const sprint of sprints) { lines.push(` - Name: ${sprint.name}`) lines.push(` Range: ${sprint.start}..${sprint.end}`) } return lines.join("\n") } export function serializeIssue(issue: Issue): string { const lines: string[] = [] lines.push(`ID: ${issue.id}`) lines.push(`Type: ${issue.type}`) lines.push(`Title: ${issue.title}`) lines.push(`Status: ${issue.status}`) lines.push(`Priority: ${issue.priority}`) lines.push(`Created: ${issue.created}`) if (issue.module) lines.push(`Module: ${issue.module}`) if (issue.dueStart) lines.push(`DueStart: ${issue.dueStart}`) if (issue.dueEnd) lines.push(`DueEnd: ${issue.dueEnd}`) lines.push(`Relationships: ${serializeRelationships(issue.relationships)}`) // Description with word-wrap at 80 columns, continuation indented to column 14 const INDENT = " " // 13 spaces — aligns with text after "Description: " const MAX_COL = 80 const firstPrefix = "Description: " if (issue.description) { const wrapped = wordWrap(issue.description, MAX_COL, firstPrefix.length, INDENT.length) lines.push(`${firstPrefix}${wrapped[0]}`) for (let i = 1; i < wrapped.length; i++) { lines.push(`${INDENT}${wrapped[i]}`) } } else { lines.push("Description:") } if (issue.body) { lines.push("") lines.push(issue.body) } return lines.join("\n") } export function serializeModules(modules: Module[]): string { const lines = ["Modules:"] for (const m of modules) { lines.push(` - Name: ${m.name}`) if (m.path) lines.push(` Path: ${m.path}`) } return lines.join("\n") } export function serializeBugzillaTracker(tracker: BugzillaTracker): string { const lines: string[] = [] lines.push(`URL: ${tracker.url}`) lines.push("Mappings:") for (const m of tracker.mappings) { lines.push(` - Module: ${m.module}`) lines.push(` Product: ${m.product}`) lines.push(` Component: ${m.component}`) } return lines.join("\n") } export function serializeTodoFile(todo: TodoFile): string { const parts: string[] = [] // Sprints part parts.push("--ISSUE") parts.push("Content-Type: application/sprints") parts.push(serializeSprints(todo.sprints)) // Modules part if (todo.modules?.length) { parts.push("") parts.push("--ISSUE") parts.push("Content-Type: application/modules") parts.push(serializeModules(todo.modules)) } // Bugzilla tracker part if (todo.bugzilla) { parts.push("") parts.push("--ISSUE") parts.push("Content-Type: application/bugzilla") parts.push(serializeBugzillaTracker(todo.bugzilla)) } // Issue parts for (const issue of todo.issues) { parts.push("") parts.push("--ISSUE") parts.push("Content-Type: application/issue") parts.push(serializeIssue(issue)) } return parts.join("\n") + "\n" }