161 lines
4.6 KiB
TypeScript
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"
|
|
}
|