use anyhow::Result;
use regex::Regex;
use std::io::{self, BufRead, BufReader, Write};
pub fn process_line(
line: &str,
previous_words: &[String],
substitute_char: char,
) -> Result<(String, Vec<String>)> {
let color_pattern = Regex::new(r"\x1b\[[0-9;]*[mGK]")?;
let separators = r#"[:\.,:;!?@#$%^&*()+=\[\]{}<>~/\\|"'\-]"#;
let token_pattern = Regex::new(&format!(
r"({})|([{}])|(\s+)|(\d+)|(\w+)",
color_pattern.as_str(),
separators
))?;
let current_line = line.trim_end_matches('\n');
let mut output = Vec::new();
let mut current_words = Vec::new();
for mat in token_pattern.find_iter(current_line) {
let token = mat.as_str();
if color_pattern.is_match(token) {
output.push(token.to_string());
} else if Regex::new(&format!(r"^[{}]$|^\s+$", separators))?.is_match(token) {
output.push(token.to_string());
} else {
if current_words.len() < previous_words.len()
&& token == previous_words[current_words.len()]
{
output.push(substitute_char.to_string().repeat(token.len()));
} else {
output.push(token.to_string());
}
current_words.push(token.to_string());
}
}
let output_line = output.join("");
Ok((output_line, current_words))
}
pub fn process_stdin(substitute_char: char) -> Result<()> {
let stdin = io::stdin();
let reader = BufReader::new(stdin.lock());
let mut stdout = io::stdout();
let mut previous_words = Vec::new();
for line in reader.lines() {
let line = line?;
let (processed_line, current_words) =
process_line(&line, &previous_words, substitute_char)?;
writeln!(stdout, "{}", processed_line)?;
stdout.flush()?;
previous_words = current_words;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_line_basic() {
let previous_words = vec!["hello".to_string(), "world".to_string()];
let line = "hello world test";
let (output, words) = process_line(line, &previous_words, '░').unwrap();
assert_eq!(output, "░░░░░ ░░░░░ test");
assert_eq!(words, vec!["hello", "world", "test"]);
}
#[test]
fn test_process_line_partial_match() {
let previous_words = vec!["hello".to_string()];
let line = "hello new world";
let (output, words) = process_line(line, &previous_words, '░').unwrap();
assert_eq!(output, "░░░░░ new world");
assert_eq!(words, vec!["hello", "new", "world"]);
}
#[test]
fn test_process_line_no_match() {
let previous_words = vec!["foo".to_string(), "bar".to_string()];
let line = "hello world";
let (output, words) = process_line(line, &previous_words, '░').unwrap();
assert_eq!(output, "hello world");
assert_eq!(words, vec!["hello", "world"]);
}
#[test]
fn test_process_line_with_separators() {
let previous_words = vec!["test".to_string(), "123".to_string()];
let line = "test:123,new";
let (output, words) = process_line(line, &previous_words, '░').unwrap();
assert_eq!(output, "░░░░:░░░,new");
assert_eq!(words, vec!["test", "123", "new"]);
}
#[test]
fn test_process_line_with_ansi_colors() {
let previous_words = vec!["hello".to_string()];
let line = "\x1b[31mhello\x1b[0m world";
let (output, words) = process_line(line, &previous_words, '░').unwrap();
assert_eq!(output, "\x1b[31m░░░░░\x1b[0m world");
assert_eq!(words, vec!["hello", "world"]);
}
#[test]
fn test_process_line_empty() {
let previous_words = vec![];
let line = "";
let (output, words) = process_line(line, &previous_words, '░').unwrap();
assert_eq!(output, "");
assert_eq!(words, Vec::<String>::new());
}
#[test]
fn test_process_line_whitespace_preservation() {
let previous_words = vec!["hello".to_string()];
let line = " hello world ";
let (output, words) = process_line(line, &previous_words, '░').unwrap();
assert_eq!(output, " ░░░░░ world ");
assert_eq!(words, vec!["hello", "world"]);
}
#[test]
fn test_process_line_numbers() {
let previous_words = vec!["test".to_string(), "123".to_string()];
let line = "test 123 456";
let (output, words) = process_line(line, &previous_words, '░').unwrap();
assert_eq!(output, "░░░░ ░░░ 456");
assert_eq!(words, vec!["test", "123", "456"]);
}
#[test]
fn test_process_line_mixed_separators() {
let previous_words = vec!["path".to_string(), "to".to_string()];
let line = "/path/to/file.txt";
let (output, words) = process_line(line, &previous_words, '░').unwrap();
assert_eq!(output, "/░░░░/░░/file.txt");
assert_eq!(words, vec!["path", "to", "file", "txt"]);
}
#[test]
fn test_process_line_custom_substitute_char() {
let previous_words = vec!["hello".to_string()];
let line = "hello world";
let (output, words) = process_line(line, &previous_words, '*').unwrap();
assert_eq!(output, "***** world");
assert_eq!(words, vec!["hello", "world"]);
}
}