Skip to main content

mxr_export/
markdown.rs

1use crate::ExportThread;
2use std::collections::HashSet;
3
4pub fn export_markdown(thread: &ExportThread) -> String {
5    let mut out = String::new();
6    out.push_str(&format!("# Thread: {}\n\n", thread.subject));
7
8    for msg in &thread.messages {
9        let sender = msg.from_name.as_deref().unwrap_or(&msg.from_email);
10        let date = msg.date.format("%b %d, %Y %H:%M");
11        out.push_str(&format!("## {} — {}\n\n", sender, date));
12
13        if let Some(text) = &msg.body_text {
14            out.push_str(text.trim());
15        }
16        out.push_str("\n\n");
17
18        if !msg.attachments.is_empty() {
19            out.push_str("**Attachments:**\n");
20            for att in &msg.attachments {
21                let size_kb = att.size_bytes / 1024;
22                out.push_str(&format!("- {} ({}KB)\n", att.filename, size_kb));
23            }
24            out.push('\n');
25        }
26    }
27
28    let participants: HashSet<&str> = thread
29        .messages
30        .iter()
31        .map(|m| m.from_email.as_str())
32        .collect();
33
34    out.push_str(&format!(
35        "---\nExported from mxr | {} messages | {} participants\n",
36        thread.messages.len(),
37        participants.len(),
38    ));
39
40    out
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use crate::tests::{empty_body_thread, sample_thread, single_message_thread};
47    use pretty_assertions::assert_eq;
48
49    #[test]
50    fn markdown_starts_with_thread_subject() {
51        let result = export_markdown(&sample_thread());
52        assert!(result.starts_with("# Thread: Deployment rollback plan\n"));
53    }
54
55    #[test]
56    fn markdown_uses_sender_name_when_available() {
57        let result = export_markdown(&sample_thread());
58        assert!(result.contains("## Alice —"));
59        assert!(result.contains("## Bob —"));
60    }
61
62    #[test]
63    fn markdown_falls_back_to_email_when_no_name() {
64        let result = export_markdown(&single_message_thread());
65        assert!(result.contains("## noreply@service.com —"));
66    }
67
68    #[test]
69    fn markdown_includes_deterministic_dates() {
70        let result = export_markdown(&sample_thread());
71        assert!(result.contains("Mar 17, 2026 09:30"));
72        assert!(result.contains("Mar 17, 2026 10:15"));
73    }
74
75    #[test]
76    fn markdown_includes_message_body() {
77        let result = export_markdown(&sample_thread());
78        assert!(result.contains("What's the rollback strategy"));
79        assert!(result.contains("blue-green deployment"));
80    }
81
82    #[test]
83    fn markdown_lists_attachments() {
84        let result = export_markdown(&sample_thread());
85        assert!(result.contains("**Attachments:**"));
86        assert!(result.contains("- runbook.pdf (240KB)"));
87    }
88
89    #[test]
90    fn markdown_footer_has_correct_counts() {
91        let result = export_markdown(&sample_thread());
92        assert!(result.contains("2 messages"));
93        assert!(result.contains("2 participants"));
94    }
95
96    #[test]
97    fn markdown_deduplicates_participants() {
98        // Both messages from same sender would count as 1
99        let mut thread = sample_thread();
100        thread.messages[1].from_email = "alice@example.com".into();
101        let result = export_markdown(&thread);
102        assert!(result.contains("1 participants"));
103    }
104
105    #[test]
106    fn markdown_handles_empty_body() {
107        let result = export_markdown(&empty_body_thread());
108        // Should not crash; header still present
109        assert!(result.contains("## Ghost —"));
110    }
111
112    #[test]
113    fn markdown_is_valid_structure() {
114        let result = export_markdown(&sample_thread());
115        // H1 for thread, H2 for each message, footer separator
116        let h1_count = result.matches("# Thread:").count();
117        let h2_count = result.matches("\n## ").count();
118        assert_eq!(h1_count, 1);
119        assert_eq!(h2_count, 2);
120        assert!(result.contains("\n---\n"));
121    }
122}