mime-todo-cli/lib/serializer.ts
Tiara Rodney 932d4ad420
init
2026-03-15 03:02:41 +01:00

161 lines
4.6 KiB
TypeScript

// 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"
}