Skip to main content

hypermail/
quotes.rs

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}