1pub fn smarten(input: &str) -> String {
9 let mut out = String::with_capacity(input.len());
10 let chars: Vec<char> = input.chars().collect();
11 let mut i = 0;
12 while i < chars.len() {
13 let c = chars[i];
14 let prev = if i == 0 { None } else { Some(chars[i - 1]) };
15
16 if c == '-' && i + 1 < chars.len() && chars[i + 1] == '-' {
18 let triple = i + 2 < chars.len() && chars[i + 2] == '-';
19 out.push('\u{2014}'); i += if triple { 3 } else { 2 };
21 continue;
22 }
23
24 if c == '.' && i + 2 < chars.len() && chars[i + 1] == '.' && chars[i + 2] == '.' {
26 let next_is_dot = i + 3 < chars.len() && chars[i + 3] == '.';
27 let prev_is_dot = matches!(prev, Some('.'));
28 if !next_is_dot && !prev_is_dot {
29 out.push('\u{2026}');
30 i += 3;
31 continue;
32 }
33 }
34
35 if c == '"' {
37 let opens = match prev {
38 None => true,
39 Some(p) => {
40 p.is_whitespace() || matches!(p, '(' | '[' | '{' | '\u{2014}' | '\u{2013}')
41 }
42 };
43 out.push(if opens { '\u{201C}' } else { '\u{201D}' });
44 i += 1;
45 continue;
46 }
47 if c == '\'' {
48 let between_letters = matches!(
50 (prev, chars.get(i + 1)),
51 (Some(a), Some(b)) if a.is_alphanumeric() && b.is_alphanumeric()
52 );
53 let after_letter = matches!(prev, Some(a) if a.is_alphanumeric());
54 if between_letters || after_letter {
55 out.push('\u{2019}');
56 } else {
57 let opens = match prev {
58 None => true,
59 Some(p) => p.is_whitespace() || matches!(p, '(' | '[' | '{' | '"' | '\u{201C}'),
60 };
61 out.push(if opens { '\u{2018}' } else { '\u{2019}' });
62 }
63 i += 1;
64 continue;
65 }
66
67 out.push(c);
68 i += 1;
69 }
70 out
71}
72
73#[cfg(test)]
74mod tests {
75 use super::smarten;
76
77 #[test]
78 fn em_dash() {
79 assert_eq!(smarten("a--b"), "a\u{2014}b");
80 assert_eq!(smarten("a---b"), "a\u{2014}b");
81 }
82
83 #[test]
84 fn ellipsis() {
85 assert_eq!(smarten("wait..."), "wait\u{2026}");
86 assert_eq!(smarten("x....y"), "x....y"); }
88
89 #[test]
90 fn quotes_open_close() {
91 assert_eq!(smarten("he said \"hi\""), "he said \u{201C}hi\u{201D}");
92 }
93
94 #[test]
95 fn apostrophe() {
96 assert_eq!(smarten("don't"), "don\u{2019}t");
97 assert_eq!(smarten("boys'"), "boys\u{2019}");
98 }
99
100 #[test]
101 fn single_open_quote() {
102 assert_eq!(smarten("he said 'hi'"), "he said \u{2018}hi\u{2019}");
103 }
104
105 #[test]
106 fn preserves_plain_text() {
107 assert_eq!(smarten("hello world"), "hello world");
108 }
109}