1#![deny(missing_docs)]
25
26pub fn repair(raw: &str) -> String {
28 let mut s = strip_fences(raw);
29 s = s.replace("\r\n", "\n").replace('\r', "\n");
30 s = unsmart_quotes(&s);
31 s = trim_trailing_ws(&s);
32 while s.ends_with('\n') {
33 s.pop();
34 }
35 s
36}
37
38fn strip_fences(s: &str) -> String {
39 let bytes = s.as_bytes();
40 let mut i = 0;
41 while i + 2 < bytes.len() {
42 if &bytes[i..i + 3] == b"```" {
43 let mut start = i + 3;
44 while start < bytes.len() && bytes[start] != b'\n' {
45 start += 1;
46 }
47 if start >= bytes.len() {
48 return s.to_string();
49 }
50 start += 1;
51 let mut j = start;
52 while j + 3 <= bytes.len() {
53 if &bytes[j..j + 3] == b"```" {
54 let prev = j.checked_sub(1).map(|k| bytes[k]).unwrap_or(b'\n');
55 if prev == b'\n' {
56 return s[start..j].to_string();
57 }
58 }
59 j += 1;
60 }
61 return s.to_string();
62 }
63 i += 1;
64 }
65 s.to_string()
66}
67
68fn unsmart_quotes(s: &str) -> String {
69 s.replace('\u{201C}', "\"")
70 .replace('\u{201D}', "\"")
71 .replace('\u{2018}', "'")
72 .replace('\u{2019}', "'")
73}
74
75fn trim_trailing_ws(s: &str) -> String {
76 let mut out = String::with_capacity(s.len());
77 for line in s.split_inclusive('\n') {
78 let had_nl = line.ends_with('\n');
79 let core = if had_nl { &line[..line.len() - 1] } else { line };
80 let stripped = core.trim_end_matches(|c: char| c == ' ' || c == '\t');
81 out.push_str(stripped);
82 if had_nl {
83 out.push('\n');
84 }
85 }
86 out
87}