1const SIG_SEPARATOR: &str = "-- ";
15
16fn split_quote(line: &str) -> (usize, &str) {
19 let depth = line.bytes().take_while(|&b| b == b'>').count();
20 (depth, &line[depth..])
21}
22
23pub fn unflow(text: &str, delsp: bool) -> String {
29 let mut out = String::new();
30 let mut open: Option<(usize, String)> = None;
32 for raw in text.lines() {
33 let (depth, rest) = split_quote(raw);
34 let rest = rest.strip_prefix(' ').unwrap_or(rest);
36 let flowed = rest.ends_with(' ') && rest != SIG_SEPARATOR;
37 let content = match flowed && delsp {
38 true => &rest[..rest.len() - 1],
39 false => rest,
40 };
41 match &mut open {
42 Some((d, buf)) if *d == depth => buf.push_str(content),
44 Some(_) => {
46 flush(&mut out, open.take());
47 open = Some((depth, content.to_string()));
48 }
49 None => open = Some((depth, content.to_string())),
50 }
51 if !flowed {
52 flush(&mut out, open.take());
53 }
54 }
55 flush(&mut out, open.take());
56 out
57}
58
59fn flush(out: &mut String, paragraph: Option<(usize, String)>) {
60 let Some((depth, text)) = paragraph else {
61 return;
62 };
63 for _ in 0..depth {
64 out.push('>');
65 }
66 if depth > 0 && !text.is_empty() {
67 out.push(' ');
68 }
69 out.push_str(&text);
70 out.push('\n');
71}
72
73pub fn space_stuff(text: &str) -> String {
78 let mut out = String::new();
79 for line in text.lines() {
80 if line.starts_with(' ') || line.starts_with('>') || line.starts_with("From ") {
81 out.push(' ');
82 }
83 out.push_str(line);
84 out.push('\n');
85 }
86 if !text.ends_with('\n') {
87 out.pop();
88 }
89 out
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn paragraphs_join_and_fixed_lines_stay() {
98 let text = "One flowed \nparagraph here.\n\nA fixed line.\n";
99 assert_eq!(
100 unflow(text, false),
101 "One flowed paragraph here.\n\nA fixed line.\n"
102 );
103 }
104
105 #[test]
106 fn delsp_drops_the_marking_space() {
107 assert_eq!(unflow("half \nway\n", true), "halfway\n");
108 assert_eq!(unflow("half \nway\n", false), "half way\n");
109 }
110
111 #[test]
112 fn quote_depth_bounds_a_paragraph() {
113 let text = "> quoted and \n> continued\n>> deeper \n>> line\nmine\n";
114 assert_eq!(
115 unflow(text, false),
116 "> quoted and continued\n>> deeper line\nmine\n"
117 );
118 }
119
120 #[test]
121 fn stuffing_is_undone_and_the_signature_stays_fixed() {
122 assert_eq!(unflow(" indented\n", false), " indented\n");
124 assert_eq!(unflow(" >not a quote\n", false), ">not a quote\n");
125 assert_eq!(unflow("text\n-- \nJane\n", false), "text\n-- \nJane\n");
127 }
128
129 #[test]
130 fn stuffing_marks_the_lines_that_need_it() {
131 assert_eq!(
132 space_stuff(" lead\n>quote\nFrom here\nplain\n"),
133 " lead\n >quote\n From here\nplain\n"
134 );
135 assert_eq!(space_stuff("no newline"), "no newline");
136 }
137}