1#[derive(Debug, Default)]
20pub struct TopicSections {
21 pub preamble: String,
23 pub sections: Vec<String>,
25 pub trailing: String,
27}
28
29pub fn parse_topic_sections(content: &str) -> (String, Vec<String>) {
31 let parsed = parse_topic_sections_with_tail(content);
32 (parsed.preamble, parsed.sections)
33}
34
35pub fn parse_topic_sections_with_tail(content: &str) -> TopicSections {
37 let mut preamble = String::new();
38 let mut sections: Vec<String> = Vec::new();
39 let mut current: Option<String> = None;
40 let mut found_first = false;
41 let mut after_boundary = false;
42 let mut trailing = String::new();
43
44 for line in content.lines() {
45 if line.starts_with("<!-- agent:boundary:") {
47 after_boundary = true;
48 continue;
49 }
50 if after_boundary {
51 trailing.push_str(line);
52 trailing.push('\n');
53 continue;
54 }
55
56 if line.starts_with("### Re:") || line.starts_with("#### Re:") || line.starts_with("## Re:")
57 {
58 if let Some(prev) = current.take() {
59 sections.push(prev);
60 }
61 found_first = true;
62 current = Some(format!("{}\n", line));
63 } else if found_first {
64 let section = current.get_or_insert_with(String::new);
65 section.push_str(line);
66 section.push('\n');
67 } else {
68 preamble.push_str(line);
69 preamble.push('\n');
70 }
71 }
72
73 if let Some(last) = current {
74 sections.push(last);
75 }
76
77 TopicSections {
78 preamble,
79 sections,
80 trailing,
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn parse_topic_sections_basic() {
90 let content = "intro\n### Re: a\nbody a\n### Re: b\nbody b\n";
91 let (preamble, sections) = parse_topic_sections(content);
92 assert_eq!(preamble.trim(), "intro");
93 assert_eq!(sections.len(), 2);
94 assert!(sections[0].starts_with("### Re: a"));
95 assert!(sections[1].starts_with("### Re: b"));
96 }
97
98 #[test]
99 fn parse_topic_sections_strips_boundary_marker() {
100 let content = "### Re: a\nbody a\n<!-- agent:boundary:abcd1234 -->\ntail\n";
101 let parsed = parse_topic_sections_with_tail(content);
102 assert_eq!(parsed.sections.len(), 1);
103 assert!(!parsed.sections[0].contains("boundary"));
104 assert_eq!(parsed.trailing.trim(), "tail");
105 }
106
107 #[test]
108 fn parse_topic_sections_no_re_headings() {
109 let content = "just preamble\nmore preamble\n";
110 let (preamble, sections) = parse_topic_sections(content);
111 assert!(sections.is_empty());
112 assert_eq!(preamble.trim(), "just preamble\nmore preamble");
113 }
114}