whiteout/parser/
block.rs

1use anyhow::Result;
2use once_cell::sync::Lazy;
3use regex::Regex;
4
5use super::Decoration;
6
7// Static regex compilation for performance
8static START_PATTERN: Lazy<Regex> = Lazy::new(|| {
9    // Match comment lines with just @whiteout-start (and optional whitespace)
10    Regex::new(r"(?m)^\s*(?://|#|--|/\*|\*)\s*@whiteout-start\s*(?:\*/)?$").expect("Failed to compile start pattern")
11});
12
13static END_PATTERN: Lazy<Regex> = Lazy::new(|| {
14    // Match comment lines with just @whiteout-end (and optional whitespace)
15    Regex::new(r"(?m)^\s*(?://|#|--|/\*|\*)\s*@whiteout-end\s*(?:\*/)?$").expect("Failed to compile end pattern")
16});
17
18pub struct BlockParser;
19
20impl Default for BlockParser {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl BlockParser {
27    pub fn new() -> Self {
28        // Force lazy static initialization
29        let _ = &*START_PATTERN;
30        let _ = &*END_PATTERN;
31        Self
32    }
33
34    pub fn parse(&self, content: &str) -> Result<Vec<Decoration>> {
35        let mut decorations = Vec::new();
36        let lines: Vec<&str> = content.lines().collect();
37        let mut i = 0;
38        
39        while i < lines.len() {
40            // Check if line matches pattern and is not escaped
41            if START_PATTERN.is_match(lines[i]) && !lines[i].contains(r"\@whiteout-start") {
42                let start_line = i + 1;
43                let mut local_lines = Vec::new();
44                let mut committed_lines = Vec::new();
45                
46                i += 1;
47                
48                while i < lines.len() && !END_PATTERN.is_match(lines[i]) {
49                    local_lines.push(lines[i]);
50                    i += 1;
51                }
52                
53                // Only create decoration if we found the end marker
54                if i < lines.len() && END_PATTERN.is_match(lines[i]) {
55                    let _end_marker_line = i + 1;
56                    i += 1;
57                    
58                    while i < lines.len() {
59                        if i + 1 < lines.len() && START_PATTERN.is_match(lines[i + 1]) {
60                            break;
61                        }
62                        
63                        if START_PATTERN.is_match(lines[i]) || END_PATTERN.is_match(lines[i]) {
64                            break;
65                        }
66                        
67                        committed_lines.push(lines[i]);
68                        i += 1;
69                        
70                        if !committed_lines.is_empty() && 
71                           (i >= lines.len() || lines[i].trim().is_empty() || 
72                            START_PATTERN.is_match(lines[i])) {
73                            break;
74                        }
75                    }
76                    
77                    // Push decoration even if local_lines is empty (for cleaned content)
78                    decorations.push(Decoration::Block {
79                        start_line,
80                        end_line: start_line + local_lines.len() + 1, // end_line is the line with @whiteout-end
81                        local_content: local_lines.join("\n"),
82                        committed_content: committed_lines.join("\n"),
83                    });
84                }
85            } else {
86                i += 1;
87            }
88        }
89        
90        Ok(decorations)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_block_parser() {
100        let parser = BlockParser::new();
101        let content = r#"
102// @whiteout-start
103const DEBUG = true;
104const LOG_LEVEL = "trace";
105// @whiteout-end
106const DEBUG = false;
107const LOG_LEVEL = "error";
108"#;
109        
110        let decorations = parser.parse(content).unwrap();
111        assert_eq!(decorations.len(), 1);
112        
113        match &decorations[0] {
114            Decoration::Block { start_line, end_line: _, local_content, committed_content } => {
115                assert_eq!(*start_line, 2);
116                assert!(local_content.contains("true"));
117                assert!(committed_content.contains("false"));
118            }
119            _ => panic!("Expected block decoration"),
120        }
121    }
122
123    #[test]
124    fn test_incomplete_block_not_matched() {
125        let parser = BlockParser::new();
126        let content = r#"
127// @whiteout-start
128const SECRET = "value";
129// Missing @whiteout-end
130const OTHER = "data";
131"#;
132        
133        let decorations = parser.parse(content).unwrap();
134        // Should not find any decorations since block is incomplete
135        assert_eq!(decorations.len(), 0, "Should not match incomplete blocks");
136    }
137    
138    #[test]
139    fn test_multiple_blocks() {
140        let parser = BlockParser::new();
141        let content = r#"
142// @whiteout-start
143let x = 1;
144// @whiteout-end
145let x = 2;
146
147// @whiteout-start
148let y = 3;
149// @whiteout-end
150let y = 4;
151"#;
152        
153        let decorations = parser.parse(content).unwrap();
154        assert_eq!(decorations.len(), 2);
155    }
156}