init
This commit is contained in:
commit
932d4ad420
46 changed files with 5800 additions and 0 deletions
161
lib/serializer.ts
Normal file
161
lib/serializer.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
// 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"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue