/// Domain-aware output compaction: strips ANSI codes and normalises whitespace.
pub fn strip_ansi(input: &str) -> String {
    let mut out = Vec::new();
    strip_ansi_escapes::strip_str(input)
        .chars()
        .for_each(|c| out.push(c));
    out.into_iter().collect()
}

pub fn normalise(input: &str) -> String {
    let stripped = strip_ansi(input);
    let joined = stripped
        .lines()
        .map(|l| l.trim_end())
        .collect::<Vec<_>>()
        .join("\n");
    if input.ends_with('\n') {
        format!("{joined}\n")
    } else {
        joined
    }
}

/// Remove consecutive blank lines, keeping at most one.
pub fn collapse_blanks(input: &str) -> String {
    let mut prev_blank = false;
    let mut result = Vec::new();
    for line in input.lines() {
        let blank = line.trim().is_empty();
        if blank && prev_blank {
            continue;
        }
        result.push(line);
        prev_blank = blank;
    }
    let joined = result.join("\n");
    if input.ends_with('\n') {
        format!("{joined}\n")
    } else {
        joined
    }
}
