buup/transformers/
line_number_remover.rs

1use crate::{Transform, TransformError, TransformerCategory};
2
3/// Removes leading line numbers (and optional whitespace) from each line.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct LineNumberRemover;
6
7/// Default test input for Line Number Remover
8pub const DEFAULT_TEST_INPUT: &str = "1. First line\n2. Second line\n3. Third line";
9
10impl Transform for LineNumberRemover {
11    fn name(&self) -> &'static str {
12        "Line Number Remover"
13    }
14
15    fn id(&self) -> &'static str {
16        "linenumberremover"
17    }
18
19    fn description(&self) -> &'static str {
20        "Removes line numbers (and optional delimiters) from the beginning of each line."
21    }
22
23    fn category(&self) -> TransformerCategory {
24        TransformerCategory::Formatter
25    }
26
27    fn default_test_input(&self) -> &'static str {
28        ""
29    }
30
31    fn transform(&self, input: &str) -> Result<String, TransformError> {
32        let mut output = String::new();
33        for (i, line) in input.lines().enumerate() {
34            // Find the first non-digit, non-whitespace character
35            let trimmed_line = line.trim_start();
36            let first_char_idx = trimmed_line
37                .find(|c: char| !c.is_ascii_digit())
38                .unwrap_or(trimmed_line.len());
39
40            // Check if the characters before it are all digits
41            if trimmed_line[..first_char_idx]
42                .chars()
43                .all(|c| c.is_ascii_digit())
44            {
45                // Skip the number and the following whitespace/punctuation
46                let content_start_idx = trimmed_line[first_char_idx..]
47                    .find(|c: char| !c.is_whitespace() && !matches!(c, '.' | ':' | '-' | ')'))
48                    .map(|idx| first_char_idx + idx)
49                    .unwrap_or(trimmed_line.len()); // If only number/whitespace/punct, result is empty line
50                output.push_str(&trimmed_line[content_start_idx..]);
51            } else {
52                // Line doesn't start with a number, keep it as is (minus original leading whitespace)
53                output.push_str(line.trim_start()); // Keep the original line if no number prefix
54            }
55
56            // Add newline back unless it's the last line and the input didn't end with a newline
57            if i < input.lines().count() - 1 || input.ends_with('\n') {
58                output.push('\n');
59            }
60        }
61        // Handle case where input is empty
62        if input.is_empty() {
63            return Ok("".to_string());
64        }
65        // Handle case where input contains only newlines
66        if output.is_empty() && input.chars().all(|c| c == '\n') {
67            return Ok(input.to_string()); // Return the original newlines
68        } else if !input.ends_with('\n') && output.ends_with('\n') {
69            // If the original didn't end with newline but we added one, remove it.
70            output.pop();
71        }
72
73        Ok(output)
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_line_number_remover() {
83        let transformer = LineNumberRemover;
84        assert_eq!(
85            transformer.transform(DEFAULT_TEST_INPUT).unwrap(),
86            "First line\nSecond line\nThird line"
87        );
88        assert_eq!(
89            transformer.transform("1 Hello\n2 World").unwrap(),
90            "Hello\nWorld"
91        );
92        assert_eq!(
93            transformer
94                .transform("1. First line\n2. Second line\n")
95                .unwrap(),
96            "First line\nSecond line\n"
97        );
98        assert_eq!(
99            transformer.transform("3:\tThird line").unwrap(),
100            "Third line"
101        );
102        assert_eq!(
103            transformer.transform("No leading number").unwrap(),
104            "No leading number"
105        );
106        assert_eq!(transformer.transform("").unwrap(), "");
107        assert_eq!(transformer.transform("1 \n2 \n").unwrap(), "\n\n"); // Lines with only numbers
108        assert_eq!(
109            transformer.transform("1 Line1\n\n3 Line3").unwrap(),
110            "Line1\n\nLine3"
111        ); // Skips empty line
112        assert_eq!(transformer.transform("  4) Item 4").unwrap(), "Item 4"); // Leading whitespace and parenthesis
113        assert_eq!(transformer.transform("5.").unwrap(), ""); // Only number and dot
114        assert_eq!(
115            transformer
116                .transform("Line without number\n6 Line with number")
117                .unwrap(),
118            "Line without number\nLine with number"
119        );
120        assert_eq!(transformer.transform("10- Item ten").unwrap(), "Item ten");
121    }
122}