whiteout/parser/
simple.rs

1use anyhow::Result;
2use once_cell::sync::Lazy;
3use regex::Regex;
4
5use super::Decoration;
6
7// Static regex compilation for performance
8// Match lines that have @whiteout as a standalone decoration (not part of other text)
9static PATTERN: Lazy<Regex> = Lazy::new(|| {
10    Regex::new(r"(?m)^\s*(?://|#|--|/\*|\*)\s*@whiteout\s*(?:\*/)?$").expect("Failed to compile pattern")
11});
12
13/// Parser for simple @whiteout lines that hide entire lines or blocks
14pub struct SimpleParser;
15
16impl Default for SimpleParser {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl SimpleParser {
23    pub fn new() -> Self {
24        // Force lazy static initialization
25        let _ = &*PATTERN;
26        Self
27    }
28
29    pub fn parse(&self, content: &str) -> Result<Vec<Decoration>> {
30        let mut decorations = Vec::new();
31        let lines: Vec<&str> = content.lines().collect();
32        let mut i = 0;
33        
34        while i < lines.len() {
35            // Check if line matches pattern and is not escaped
36            // Also skip @whiteout-start, @whiteout-end, @whiteout:, and @whiteout-partial patterns
37            if PATTERN.is_match(lines[i]) 
38                && !lines[i].contains(r"\@whiteout")
39                && !lines[i].contains("@whiteout-start")
40                && !lines[i].contains("@whiteout-end")
41                && !lines[i].contains("@whiteout:")
42                && !lines[i].contains("@whiteout-partial") {
43                let start_line = i + 1;
44                
45                // The @whiteout line itself is the marker
46                // Only the next immediate line is local content
47                i += 1;
48                
49                if i < lines.len() {
50                    // Only capture the single next line
51                    let local_content = lines[i].to_string();
52                    
53                    decorations.push(Decoration::Block {
54                        start_line,
55                        end_line: start_line + 1, // Only one line
56                        local_content,
57                        committed_content: String::new(), // Nothing in committed version
58                    });
59                    
60                    i += 1; // Move past the hidden line
61                }
62                // Don't increment i here since we already did in the loop
63            } else {
64                i += 1;
65            }
66        }
67        
68        Ok(decorations)
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn test_simple_parser() {
78        let parser = SimpleParser::new();
79        let content = r#"normal line
80@whiteout
81this will be hidden
82this stays visible
83
84normal again"#;
85        
86        let decorations = parser.parse(content).unwrap();
87        assert_eq!(decorations.len(), 1);
88        
89        match &decorations[0] {
90            Decoration::Block { local_content, committed_content, .. } => {
91                assert_eq!(local_content, "this will be hidden");
92                assert!(!local_content.contains("this stays visible"));
93                assert!(committed_content.is_empty());
94            }
95            _ => panic!("Expected block decoration"),
96        }
97    }
98}