pub fn normalize_output(input: &str) -> String {
normalize_output_with_shift(input).0
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LineShift {
deleted: Vec<u32>,
}
impl LineShift {
pub fn map(&self, pre_line: u32) -> Option<u32> {
if self.deleted.binary_search(&pre_line).is_ok() {
return None;
}
let deleted_before = self.deleted.partition_point(|&d| d < pre_line) as u32;
Some(pre_line - deleted_before)
}
}
pub fn normalize_output_with_shift(input: &str) -> (String, LineShift) {
let input_lf = input.replace("\r\n", "\n");
let lines: Vec<&str> = input_lf.lines().collect();
let mut result_lines: Vec<&str> = Vec::with_capacity(lines.len());
let mut surviving: Vec<u32> = Vec::with_capacity(lines.len());
let mut deleted: Vec<u32> = Vec::new();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim();
if trimmed.is_empty() {
let mut j = i + 1;
let mut found_end = false;
while j < lines.len() {
let next_trimmed = lines[j].trim();
if next_trimmed.is_empty() {
j += 1;
continue;
}
if next_trimmed == "end" {
found_end = true;
}
break;
}
if found_end {
deleted.push((i + 1) as u32);
i += 1;
continue;
}
}
result_lines.push(line);
surviving.push((i + 1) as u32);
i += 1;
}
let processed = result_lines.join("\n");
let trimmed = processed.trim_end_matches([' ', '\t', '\r', '\n']);
let last_kept = result_lines.iter().rposition(|l| !l.trim().is_empty());
match last_kept {
Some(k) => {
for &pre in &surviving[k + 1..] {
deleted.push(pre);
}
}
None => {
for &pre in &surviving {
deleted.push(pre);
}
}
}
deleted.sort_unstable();
(format!("{}\n", trimmed), LineShift { deleted })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_empty_input() {
assert_eq!(normalize_output(""), "\n");
}
#[test]
fn test_normalize_existing_single_newline() {
assert_eq!(normalize_output("code\n"), "code\n");
}
#[test]
fn test_normalize_single_extra_blank_line() {
assert_eq!(normalize_output("code\n\n"), "code\n");
}
#[test]
fn test_normalize_multiple_extra_blank_lines() {
assert_eq!(normalize_output("code\n\n\n"), "code\n");
}
#[test]
fn test_normalize_preserves_intermediate_blank_lines() {
assert_eq!(normalize_output("a\n\nb\n\n"), "a\n\nb\n");
}
#[test]
fn test_normalize_crlf_input() {
assert_eq!(normalize_output("code\r\n\r\n"), "code\n");
}
#[test]
fn test_normalize_mixed_line_endings() {
assert_eq!(normalize_output("a\r\nb\n\r\n\n"), "a\nb\n");
}
#[test]
fn test_normalize_trailing_whitespace_only() {
assert_eq!(normalize_output("code\n \t\n"), "code\n");
}
#[test]
fn test_normalize_no_trailing_newline() {
assert_eq!(normalize_output("code"), "code\n");
}
#[test]
fn test_normalize_multi_line_content() {
let input = "line1\nline2\nline3\n\n\n";
assert_eq!(normalize_output(input), "line1\nline2\nline3\n");
}
#[test]
fn test_normalize_blank_line_before_end() {
let input = " end\n\nend\n";
assert_eq!(normalize_output(input), " end\nend\n");
}
#[test]
fn test_normalize_multiple_blank_lines_before_end() {
let input = " end\n\n\nend\n";
assert_eq!(normalize_output(input), " end\nend\n");
}
#[test]
fn test_normalize_blank_line_before_indented_end() {
let input = "code\n\n end\n";
assert_eq!(normalize_output(input), "code\n end\n");
}
#[test]
fn test_normalize_lua_do_block() {
let input = "do\n function f()\n return 1\n end\n\nend\n";
assert_eq!(
normalize_output(input),
"do\n function f()\n return 1\n end\nend\n"
);
}
#[test]
fn test_normalize_nested_end_blocks() {
let input = " end\n\n end\n\nend\n";
assert_eq!(normalize_output(input), " end\n end\nend\n");
}
#[test]
fn test_with_shift_byte_identical_to_legacy() {
let inputs = [
"",
"code\n",
"code\n\n",
"code\n\n\n",
"a\n\nb\n\n",
"code\r\n\r\n",
"a\r\nb\n\r\n\n",
"code\n \t\n",
"code",
"line1\nline2\nline3\n\n\n",
" end\n\nend\n",
" end\n\n\nend\n",
"code\n\n end\n",
"do\n function f()\n return 1\n end\n\nend\n",
" end\n\n end\n\nend\n",
];
for input in inputs {
let (s, _shift) = normalize_output_with_shift(input);
assert_eq!(
s,
normalize_output(input),
"byte mismatch for input {input:?}"
);
}
}
#[test]
fn test_shift_blank_before_end_mapping() {
let input = "code\n\nend\n";
let (s, shift) = normalize_output_with_shift(input);
assert_eq!(s, "code\nend\n");
assert_eq!(shift.map(1), Some(1)); assert_eq!(shift.map(2), None); assert_eq!(shift.map(3), Some(2)); }
#[test]
fn test_shift_multiple_blanks_before_end_mapping() {
let input = " end\n\n\nend\n";
let (s, shift) = normalize_output_with_shift(input);
assert_eq!(s, " end\nend\n");
assert_eq!(shift.map(1), Some(1));
assert_eq!(shift.map(2), None);
assert_eq!(shift.map(3), None);
assert_eq!(shift.map(4), Some(2));
assert!(shift.map(1).unwrap() < shift.map(4).unwrap());
}
#[test]
fn test_shift_trailing_blank_lines_mapping() {
let input = "line1\nline2\n\n\n";
let (s, shift) = normalize_output_with_shift(input);
assert_eq!(s, "line1\nline2\n");
assert_eq!(shift.map(1), Some(1));
assert_eq!(shift.map(2), Some(2));
assert_eq!(shift.map(3), None);
assert_eq!(shift.map(4), None);
}
#[test]
fn test_shift_inline_trailing_whitespace_not_a_deletion() {
let input = "code\n \t\n";
let (s, shift) = normalize_output_with_shift(input);
assert_eq!(s, "code\n");
assert_eq!(shift.map(1), Some(1)); assert_eq!(shift.map(2), None); }
#[test]
fn test_shift_combined_body_and_trailing_deletions() {
let input = "do\n f()\n\nend\n\n\n";
let (s, shift) = normalize_output_with_shift(input);
assert_eq!(s, "do\n f()\nend\n");
assert_eq!(shift.map(1), Some(1));
assert_eq!(shift.map(2), Some(2));
assert_eq!(shift.map(3), None);
assert_eq!(shift.map(4), Some(3));
assert_eq!(shift.map(5), None);
assert_eq!(shift.map(6), None);
let survivors: Vec<u32> = (1..=6).filter_map(|p| shift.map(p)).collect();
assert_eq!(survivors, vec![1, 2, 3]);
assert!(survivors.windows(2).all(|w| w[0] < w[1]));
}
#[test]
fn test_shift_empty_input() {
let (s, shift) = normalize_output_with_shift("");
assert_eq!(s, "\n");
assert_eq!(shift, LineShift::default()); }
#[test]
fn test_shift_all_blank_lines_deleted() {
let input = "\n \n\t\n";
let (s, shift) = normalize_output_with_shift(input);
assert_eq!(s, "\n");
assert_eq!(shift.map(1), None);
assert_eq!(shift.map(2), None);
assert_eq!(shift.map(3), None);
}
#[test]
fn test_shift_no_deletions_identity() {
let input = "a\n\nb\nc\n";
let (s, shift) = normalize_output_with_shift(input);
assert_eq!(s, "a\n\nb\nc\n");
assert_eq!(shift.map(1), Some(1));
assert_eq!(shift.map(2), Some(2)); assert_eq!(shift.map(3), Some(3));
assert_eq!(shift.map(4), Some(4));
}
}