pub const MIN_RUN: usize = 3;
pub const HEADER_PREFIX: &str = "[lean-ctx: ";
const HEADER_SUFFIX: &str = " repeated lines collapsed, lossless]";
const MARKER_PREFIX: &str = "[×";
const MARKER_SUFFIX: &str = " identical lines]";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Collapsed {
pub text: String,
pub lines_before: usize,
pub lines_after: usize,
}
fn marker_count(line: &str) -> Option<usize> {
line.strip_prefix(MARKER_PREFIX)?
.strip_suffix(MARKER_SUFFIX)?
.parse()
.ok()
}
fn is_reserved(line: &str) -> bool {
marker_count(line).is_some()
|| (line.starts_with(HEADER_PREFIX) && line.ends_with(HEADER_SUFFIX))
}
#[must_use]
pub fn collapse(input: &str) -> Option<Collapsed> {
let lines: Vec<&str> = input.split('\n').collect();
if lines.iter().any(|l| is_reserved(l)) {
return None;
}
let mut out: Vec<String> = Vec::with_capacity(lines.len());
let mut removed = 0usize;
let mut i = 0;
while i < lines.len() {
let mut j = i + 1;
while j < lines.len() && lines[j] == lines[i] {
j += 1;
}
let run = j - i;
if run >= MIN_RUN {
out.push(lines[i].to_string());
out.push(format!("{MARKER_PREFIX}{run}{MARKER_SUFFIX}"));
removed += run - 2;
} else {
out.extend(lines[i..j].iter().map(|l| (*l).to_string()));
}
i = j;
}
if removed == 0 {
return None;
}
let body = out.join("\n");
Some(Collapsed {
text: format!("{HEADER_PREFIX}{removed}{HEADER_SUFFIX}\n{body}"),
lines_before: lines.len(),
lines_after: out.len() + 1,
})
}
#[must_use]
pub fn expand(collapsed: &str) -> String {
let Some((header, body)) = collapsed.split_once('\n') else {
return collapsed.to_string();
};
if !(header.starts_with(HEADER_PREFIX) && header.ends_with(HEADER_SUFFIX)) {
return collapsed.to_string();
}
let mut out: Vec<&str> = Vec::new();
for line in body.split('\n') {
match (marker_count(line), out.last().copied()) {
(Some(n), Some(prev)) if n >= MIN_RUN => {
out.extend(std::iter::repeat_n(prev, n - 1));
}
_ => out.push(line),
}
}
out.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
fn generated_inputs() -> Vec<String> {
let atoms = [
"a",
"b",
"",
"Compiling x",
"\r50%\r100%",
" indented",
"a ",
];
let mut seed: u64 = 0x9E37_79B9_7F4A_7C15;
let mut out = Vec::new();
for _ in 0..2000 {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
let len = (seed % 40) as usize;
let mut lines = Vec::with_capacity(len);
let mut s = seed;
for _ in 0..len {
s = s.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
if !s.is_multiple_of(3) && !lines.is_empty() {
let prev: &str = lines[lines.len() - 1];
lines.push(prev);
} else {
lines.push(atoms[(s >> 33) as usize % atoms.len()]);
}
}
let mut text = lines.join("\n");
if seed.is_multiple_of(2) {
text.push('\n');
}
out.push(text);
}
out
}
#[test]
fn expand_inverts_collapse_on_generated_inputs() {
let mut collapsed_any = 0;
for input in generated_inputs() {
if let Some(c) = collapse(&input) {
collapsed_any += 1;
assert_eq!(expand(&c.text), input, "round trip must be exact");
assert!(c.lines_after <= c.lines_before);
}
}
assert!(collapsed_any > 100, "generator must exercise collapses");
}
#[test]
fn collapse_table() {
let cases: &[(&str, Option<&str>)] = &[
("a\nb\nc", None),
("a\na", None),
("a\na\na", Some("a\n[×3 identical lines]")),
(
"x\na\na\na\na\ny\n",
Some("x\na\n[×4 identical lines]\ny\n"),
),
("a\n\n\n\n", Some("a\n\n[×4 identical lines]")),
("a\na \na\na ", None),
("[×3 identical lines]\nz\nz\nz", None),
];
for (input, want) in cases {
let got = collapse(input).map(|c| {
let (_, body) = c.text.split_once('\n').unwrap();
body.to_string()
});
assert_eq!(got.as_deref(), *want, "input {input:?}");
if let Some(c) = collapse(input) {
assert_eq!(expand(&c.text), *input);
}
}
}
#[test]
fn header_counts_removed_lines() {
let c = collapse(&"same\n".repeat(10)).unwrap();
assert!(
c.text
.starts_with("[lean-ctx: 8 repeated lines collapsed, lossless]\n")
);
}
#[test]
fn expand_leaves_plain_text_alone() {
for t in ["", "plain", "a\n[×3 identical lines]"] {
assert_eq!(expand(t), t);
}
}
}