1use crate::message::BodyChain;
2
3const QUOTE_PREFIXES: &[&str] = &["> ", ">", "| ", "|", ": ", ":"];
4
5pub fn find_quote_prefix(bodies: &BodyChain) -> Option<&'static str> {
6 let mut counts: Vec<(&str, usize)> = QUOTE_PREFIXES.iter().map(|p| (*p, 0)).collect();
7
8 for body in &bodies.bodies {
9 if body.attached || body.header {
10 continue;
11 }
12 for (prefix, count) in &mut counts {
13 if body.line.starts_with(*prefix) {
14 *count += 1;
15 }
16 }
17 }
18
19 counts
20 .into_iter()
21 .filter(|(_, c)| *c > 0)
22 .max_by_key(|(_, c)| *c)
23 .map(|(p, _)| p)
24}
25
26pub fn get_quote_prefix() -> &'static str {
27 "> "
28}
29
30pub fn is_quote(line: &str) -> bool {
31 QUOTE_PREFIXES.iter().any(|p| line.starts_with(p))
32}
33
34pub fn unquote(line: &str) -> String {
35 for prefix in QUOTE_PREFIXES {
36 if let Some(rest) = line.strip_prefix(prefix) {
37 return rest.to_string();
38 }
39 }
40 line.to_string()
41}
42
43pub fn find_quote_depth(line: &str) -> i32 {
44 let mut depth = 0;
45 let mut s = line;
46 loop {
47 let mut found = false;
48 for prefix in QUOTE_PREFIXES {
49 if let Some(rest) = s.strip_prefix(prefix) {
50 depth += 1;
51 s = rest;
52 found = true;
53 break;
54 }
55 }
56 if !found {
57 break;
58 }
59 }
60 depth
61}
62
63pub fn find_quote_class(line: &str) -> String {
64 let depth = find_quote_depth(line).min(9);
65 if depth > 0 {
66 format!("hm-quote-{}", depth)
67 } else {
68 String::new()
69 }
70}
71
72pub fn compute_quoted_percent(bodies: &BodyChain) -> i32 {
73 let total = bodies.bodies.iter().filter(|b| !b.attached && !b.header).count();
74 if total == 0 {
75 return 0;
76 }
77 let quoted = bodies
78 .bodies
79 .iter()
80 .filter(|b| !b.attached && !b.header && is_quote(&b.line))
81 .count();
82 (quoted * 100 / total) as i32
83}
84
85pub fn is_sig_start(line: &str) -> bool {
86 line == "-- " || line == "-- \r" || line == "--" || line == "--\r"
87}
88
89pub fn remove_hypermail_tags(line: &str) -> String {
90 if line.contains("class=\"hm-") || line.contains("<a name=") {
91 let mut result = String::with_capacity(line.len());
92 let mut skip_anchor = false;
93 let mut i = 0;
94 let chars: Vec<char> = line.chars().collect();
95
96 while i < chars.len() {
97 if chars[i] == '<' {
98 if i + 7 <= chars.len() {
99 let tag: String = chars[i..i + 7].iter().collect();
100 if tag.to_lowercase() == "<a name" {
101 skip_anchor = true;
102 i += 1;
103 continue;
104 }
105 }
106 if i + 4 <= chars.len() {
107 let end_a: String = chars[i..i + 4].iter().collect();
108 if end_a.to_lowercase() == "</a>" && skip_anchor {
109 skip_anchor = false;
110 i += 4;
111 continue;
112 }
113 }
114 }
115 if !skip_anchor {
116 result.push(chars[i]);
117 }
118 i += 1;
119 }
120 result
121 } else {
122 line.to_string()
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::message::{Body, BodyChain};
130
131 fn make_body(lines: &[&str]) -> BodyChain {
132 BodyChain {
133 bodies: lines
134 .iter()
135 .map(|l| Body {
136 line: l.to_string(),
137 html: false,
138 header: false,
139 parsed_header: false,
140 attached: false,
141 demimed: false,
142 msgnum: 0,
143 })
144 .collect(),
145 }
146 }
147
148 #[test]
149 fn test_is_quote() {
150 assert!(is_quote("> quoted text"));
151 assert!(is_quote(">quoted"));
152 assert!(!is_quote("not quoted"));
153 }
154
155 #[test]
156 fn test_unquote() {
157 assert_eq!(unquote("> text"), "text");
158 assert_eq!(unquote(">text"), "text");
159 assert_eq!(unquote("no prefix"), "no prefix");
160 }
161
162 #[test]
163 fn test_quote_depth() {
164 assert_eq!(find_quote_depth(">>> deep"), 3);
165 assert_eq!(find_quote_depth("> single"), 1);
166 assert_eq!(find_quote_depth("none"), 0);
167 }
168
169 #[test]
170 fn test_compute_quoted_percent() {
171 let body = make_body(&["> q1", "not q", "> q2", "not q"]);
172 assert_eq!(compute_quoted_percent(&body), 50);
173 }
174
175 #[test]
176 fn test_is_sig_start() {
177 assert!(is_sig_start("-- "));
178 assert!(is_sig_start("--"));
179 assert!(!is_sig_start("not sig"));
180 assert!(!is_sig_start("---"));
181 }
182
183 #[test]
184 fn test_find_quote_prefix() {
185 let body = make_body(&["> a", "> b", "normal", "> c"]);
186 let prefix = find_quote_prefix(&body);
187 assert!(prefix.is_some());
188 }
189
190 #[test]
191 fn test_get_quote_prefix() {
192 assert_eq!(get_quote_prefix(), "> ");
193 }
194
195 #[test]
196 fn test_find_quote_class_depth_1() {
197 assert_eq!(find_quote_class("> text"), "hm-quote-1");
198 }
199
200 #[test]
201 fn test_find_quote_class_depth_3() {
202 assert_eq!(find_quote_class(">>> text"), "hm-quote-3");
203 }
204
205 #[test]
206 fn test_find_quote_class_non_quote() {
207 assert_eq!(find_quote_class("normal text"), "");
208 }
209
210 #[test]
211 fn test_remove_hypermail_tags_strips_anchor() {
212 let line = r#"<a name="msg1"></a>Some text"#;
213 let result = remove_hypermail_tags(line);
214 assert!(!result.contains("<a name="));
215 assert!(result.contains("Some text"));
216 }
217
218 #[test]
219 fn test_remove_hypermail_tags_no_tags_unchanged() {
220 let line = "plain text without tags";
221 assert_eq!(remove_hypermail_tags(line), line);
222 }
223
224 #[test]
225 fn test_compute_quoted_percent_zero() {
226 let body = make_body(&["line 1", "line 2", "line 3"]);
227 assert_eq!(compute_quoted_percent(&body), 0);
228 }
229
230 #[test]
231 fn test_compute_quoted_percent_empty() {
232 let body = BodyChain { bodies: Vec::new() };
233 assert_eq!(compute_quoted_percent(&body), 0);
234 }
235
236 #[test]
237 fn test_is_sig_start_cr_variant() {
238 assert!(is_sig_start("-- \r"));
239 }
240
241 #[test]
242 fn test_find_quote_prefix_no_quotes() {
243 let body = make_body(&["normal", "also normal"]);
244 let prefix = find_quote_prefix(&body);
245 assert!(prefix.is_none());
246 }
247}