| 1 |
1 |
|
/// Normalize whitespace in the final markdown output.
|
| 2 |
2 |
|
///
|
|
3 |
+ |
/// - Remove trailing whitespace from each line
|
| 3 |
4 |
|
/// - Collapse runs of 3+ newlines into 2 (one blank line)
|
| 4 |
5 |
|
/// - Trim leading/trailing whitespace
|
| 5 |
|
- |
/// - Remove trailing whitespace from each line
|
|
6 |
+ |
///
|
|
7 |
+ |
/// The per-line trim runs FIRST, and the order is the whole point. Trimming
|
|
8 |
+ |
/// turns a line holding only spaces into an empty one, which is to say it
|
|
9 |
+ |
/// *creates* blank lines: `"a\n \n\nb"` has no run of three newlines until the
|
|
10 |
+ |
/// space is gone, and then it has one. Collapsing first and trimming second
|
|
11 |
+ |
/// therefore emits `\n\n\n` from input that looked clean when the collapse
|
|
12 |
+ |
/// examined it, and nothing runs afterwards to catch it.
|
|
13 |
+ |
///
|
|
14 |
+ |
/// That is not hypothetical. It is how `<pre/><strong/><h1/><br> </br>` reached
|
|
15 |
+ |
/// `"```\n**\n\n#\n\n\n**\n```"` -- the `<br> </br>` contributes the whitespace-only
|
|
16 |
+ |
/// line, and every sibling counterexample the amplified property test found is
|
|
17 |
+ |
/// the same shape with a different element supplying the space.
|
| 6 |
18 |
|
pub(crate) fn normalize(input: &str) -> String {
|
| 7 |
|
- |
let mut result = String::with_capacity(input.len());
|
|
19 |
+ |
// Trailing whitespace goes first, so the collapse below sees the blank
|
|
20 |
+ |
// lines this creates rather than the whitespace that was hiding them.
|
|
21 |
+ |
let trimmed: Vec<&str> = input.lines().map(str::trim_end).collect();
|
|
22 |
+ |
let trimmed = trimmed.join("\n");
|
|
23 |
+ |
|
|
24 |
+ |
let mut result = String::with_capacity(trimmed.len());
|
| 8 |
25 |
|
let mut consecutive_newlines = 0u32;
|
| 9 |
26 |
|
|
| 10 |
|
- |
for ch in input.chars() {
|
|
27 |
+ |
for ch in trimmed.chars() {
|
| 11 |
28 |
|
if ch == '\n' {
|
| 12 |
29 |
|
consecutive_newlines += 1;
|
| 13 |
30 |
|
if consecutive_newlines <= 2 {
|
| 19 |
36 |
|
}
|
| 20 |
37 |
|
}
|
| 21 |
38 |
|
|
| 22 |
|
- |
// Trim trailing whitespace from each line
|
| 23 |
|
- |
let lines: Vec<&str> = result.lines().map(str::trim_end).collect();
|
| 24 |
|
- |
let joined = lines.join("\n");
|
| 25 |
|
- |
joined.trim().to_string()
|
|
39 |
+ |
result.trim().to_string()
|
| 26 |
40 |
|
}
|
| 27 |
41 |
|
|
| 28 |
42 |
|
#[cfg(test)]
|
| 53 |
67 |
|
fn empty_input() {
|
| 54 |
68 |
|
assert_eq!(normalize(""), "");
|
| 55 |
69 |
|
}
|
|
70 |
+ |
|
|
71 |
+ |
#[test]
|
|
72 |
+ |
fn a_whitespace_only_line_between_blanks_does_not_survive_as_a_third_newline() {
|
|
73 |
+ |
// The line holding one space is not a blank line until it is trimmed,
|
|
74 |
+ |
// so a collapse that runs before the trim never sees the run of three
|
|
75 |
+ |
// this becomes. Trimming first is what makes the collapse honest.
|
|
76 |
+ |
assert_eq!(normalize("a\n \n\nb"), "a\n\nb");
|
|
77 |
+ |
assert_eq!(normalize("a\n\n \nb"), "a\n\nb");
|
|
78 |
+ |
assert_eq!(normalize("a\n \n \n \nb"), "a\n\nb");
|
|
79 |
+ |
}
|
|
80 |
+ |
|
|
81 |
+ |
#[test]
|
|
82 |
+ |
fn no_output_ever_carries_three_consecutive_newlines() {
|
|
83 |
+ |
for input in [
|
|
84 |
+ |
"a\n \n\nb",
|
|
85 |
+ |
"**\n\n#\n \n\n**",
|
|
86 |
+ |
"x\n\t\n\ny",
|
|
87 |
+ |
"p\n \n \n \nq",
|
|
88 |
+ |
] {
|
|
89 |
+ |
let out = normalize(input);
|
|
90 |
+ |
assert!(
|
|
91 |
+ |
!out.contains("\n\n\n"),
|
|
92 |
+ |
"triple newline survived for {input:?}: {out:?}"
|
|
93 |
+ |
);
|
|
94 |
+ |
}
|
|
95 |
+ |
}
|
| 56 |
96 |
|
}
|