Skip to main content

hj_render/
lib.rs

1use hj_core::{Handoff, HandoffItem, HandoffState};
2
3pub fn render_markdown(handoff: &Handoff, state: Option<&HandoffState>) -> String {
4    let project = handoff.project.as_deref().unwrap_or("unknown");
5    let updated = handoff.updated.as_deref().unwrap_or("unknown");
6    let branch = state
7        .and_then(|value| value.branch.as_deref())
8        .unwrap_or("unknown");
9    let build = state
10        .and_then(|value| value.build.as_deref())
11        .unwrap_or("unknown");
12    let tests = state
13        .and_then(|value| value.tests.as_deref())
14        .unwrap_or("unknown");
15
16    let mut out = String::new();
17    out.push_str(&format!("# Handoff — {project} ({updated})\n\n"));
18    out.push_str(&format!(
19        "**Branch:** {branch} | **Build:** {build} | **Tests:** {tests}\n"
20    ));
21    if let Some(notes) = state
22        .and_then(|value| value.notes.as_deref())
23        .filter(|notes| !notes.is_empty())
24    {
25        out.push_str(&format!("{notes}\n"));
26    }
27
28    out.push_str("\n## Items\n\n");
29    out.push_str("| ID | P | Status | Title |\n");
30    out.push_str("|---|---|---|---|\n");
31
32    let mut items: Vec<&HandoffItem> = handoff.active_items().collect();
33    items.sort_by_key(|item| {
34        (
35            priority_rank(item.priority.as_deref()),
36            status_rank(item.status.as_deref()),
37            item.id.as_str(),
38        )
39    });
40    for item in items {
41        out.push_str(&format!(
42            "| {} | {} | {} | {} |\n",
43            item.id,
44            item.priority.as_deref().unwrap_or("-"),
45            item.status.as_deref().unwrap_or("-"),
46            item.title
47        ));
48    }
49
50    out.push_str("\n## Log\n\n");
51    for entry in handoff.log.iter().take(5) {
52        let date = entry.date.as_deref().unwrap_or("unknown");
53        if entry.commits.is_empty() {
54            out.push_str(&format!("- {date}: {}\n", entry.summary));
55        } else {
56            out.push_str(&format!(
57                "- {date}: {} [{}]\n",
58                entry.summary,
59                entry.commits.join(", ")
60            ));
61        }
62    }
63
64    out
65}
66
67fn priority_rank(priority: Option<&str>) -> u8 {
68    match priority {
69        Some("P0") => 0,
70        Some("P1") => 1,
71        Some("P2") => 2,
72        _ => 9,
73    }
74}
75
76fn status_rank(status: Option<&str>) -> u8 {
77    match status {
78        Some("open") => 0,
79        Some("blocked") => 1,
80        _ => 9,
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use hj_core::{Handoff, HandoffItem, HandoffState, LogEntry};
87
88    use super::render_markdown;
89
90    #[test]
91    fn renders_summary_markdown() {
92        let handoff = Handoff {
93            project: Some("hj".into()),
94            updated: Some("2026-04-15".into()),
95            items: vec![HandoffItem {
96                id: "hj-1".into(),
97                priority: Some("P1".into()),
98                status: Some("open".into()),
99                title: "Ship reconcile".into(),
100                ..HandoffItem::default()
101            }],
102            log: vec![LogEntry {
103                date: Some("2026-04-15".into()),
104                summary: "Scaffolded workspace".into(),
105                commits: vec!["abc1234".into()],
106                ..LogEntry::default()
107            }],
108            ..Handoff::default()
109        };
110        let state = HandoffState {
111            branch: Some("main".into()),
112            build: Some("clean".into()),
113            tests: Some("passing".into()),
114            ..HandoffState::default()
115        };
116
117        let rendered = render_markdown(&handoff, Some(&state));
118        assert!(rendered.contains("# Handoff — hj (2026-04-15)"));
119        assert!(rendered.contains("| hj-1 | P1 | open | Ship reconcile |"));
120        assert!(rendered.contains("- 2026-04-15: Scaffolded workspace [abc1234]"));
121    }
122}