backlog-md/src/formatters/task-plain-text.ts

135 lines
3.8 KiB
TypeScript

import type { Task } from "../types/index.ts";
import type { ChecklistItem } from "../ui/checklist.ts";
import { transformCodePathsPlain } from "../ui/code-path.ts";
import { formatStatusWithIcon } from "../ui/status-icon.ts";
export type TaskPlainTextOptions = {
filePathOverride?: string;
};
export function formatDateForDisplay(dateStr: string): string {
if (!dateStr) return "";
const hasTime = dateStr.includes(" ") || dateStr.includes("T");
return hasTime ? dateStr : dateStr;
}
export function buildAcceptanceCriteriaItems(task: Task): ChecklistItem[] {
const items = task.acceptanceCriteriaItems ?? [];
return items
.slice()
.sort((a, b) => a.index - b.index)
.map((criterion, index) => ({
text: `#${index + 1} ${criterion.text}`,
checked: criterion.checked,
}));
}
export function formatAcceptanceCriteriaLines(items: ChecklistItem[]): string[] {
if (items.length === 0) return [];
return items.map((item) => {
const prefix = item.checked ? "- [x]" : "- [ ]";
return `${prefix} ${transformCodePathsPlain(item.text)}`;
});
}
function formatPriority(priority?: "high" | "medium" | "low"): string | null {
if (!priority) return null;
const label = priority.charAt(0).toUpperCase() + priority.slice(1);
return label;
}
function formatAssignees(assignee?: string[]): string | null {
if (!assignee || assignee.length === 0) return null;
return assignee.map((a) => (a.startsWith("@") ? a : `@${a}`)).join(", ");
}
export function formatTaskPlainText(task: Task, options: TaskPlainTextOptions = {}): string {
const lines: string[] = [];
const filePath = options.filePathOverride ?? task.filePath;
if (filePath) {
lines.push(`File: ${filePath}`);
lines.push("");
}
lines.push(`Task ${task.id} - ${task.title}`);
lines.push("=".repeat(50));
lines.push("");
lines.push(`Status: ${formatStatusWithIcon(task.status)}`);
const priorityLabel = formatPriority(task.priority);
if (priorityLabel) {
lines.push(`Priority: ${priorityLabel}`);
}
const assigneeText = formatAssignees(task.assignee);
if (assigneeText) {
lines.push(`Assignee: ${assigneeText}`);
}
if (task.reporter) {
const reporter = task.reporter.startsWith("@") ? task.reporter : `@${task.reporter}`;
lines.push(`Reporter: ${reporter}`);
}
lines.push(`Created: ${formatDateForDisplay(task.createdDate)}`);
if (task.updatedDate) {
lines.push(`Updated: ${formatDateForDisplay(task.updatedDate)}`);
}
if (task.labels?.length) {
lines.push(`Labels: ${task.labels.join(", ")}`);
}
if (task.milestone) {
lines.push(`Milestone: ${task.milestone}`);
}
if (task.parentTaskId) {
lines.push(`Parent: ${task.parentTaskId}`);
}
if (task.subtasks?.length) {
lines.push(`Subtasks: ${task.subtasks.length}`);
}
if (task.dependencies?.length) {
lines.push(`Dependencies: ${task.dependencies.join(", ")}`);
}
lines.push("");
lines.push("Description:");
lines.push("-".repeat(50));
const description = task.description?.trim();
lines.push(transformCodePathsPlain(description && description.length > 0 ? description : "No description provided"));
lines.push("");
lines.push("Acceptance Criteria:");
lines.push("-".repeat(50));
const criteriaItems = buildAcceptanceCriteriaItems(task);
if (criteriaItems.length > 0) {
lines.push(...formatAcceptanceCriteriaLines(criteriaItems));
} else {
lines.push("No acceptance criteria defined");
}
lines.push("");
const implementationPlan = task.implementationPlan?.trim();
if (implementationPlan) {
lines.push("Implementation Plan:");
lines.push("-".repeat(50));
lines.push(transformCodePathsPlain(implementationPlan));
lines.push("");
}
const implementationNotes = task.implementationNotes?.trim();
if (implementationNotes) {
lines.push("Implementation Notes:");
lines.push("-".repeat(50));
lines.push(transformCodePathsPlain(implementationNotes));
lines.push("");
}
return lines.join("\n");
}