whiteout/parser/
partial.rs

1use anyhow::Result;
2use once_cell::sync::Lazy;
3use regex::Regex;
4
5use super::{Decoration, PartialReplacement};
6
7// Static regex compilation for performance
8static PATTERN: Lazy<Regex> = Lazy::new(|| {
9    Regex::new(r"\[\[([^|]+)\|\|([^\]]+)\]\]").expect("Failed to compile pattern")
10});
11
12static DECORATOR_PATTERN: Lazy<Regex> = Lazy::new(|| {
13    Regex::new(r"//\s*@whiteout-partial").expect("Failed to compile decorator pattern")
14});
15
16pub struct PartialParser;
17
18impl Default for PartialParser {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl PartialParser {
25    pub fn new() -> Self {
26        // Force lazy static initialization
27        let _ = &*PATTERN;
28        let _ = &*DECORATOR_PATTERN;
29        Self
30    }
31
32    pub fn parse(&self, content: &str) -> Result<Vec<Decoration>> {
33        let mut decorations = Vec::new();
34        
35        for (line_num, line) in content.lines().enumerate() {
36            // Only process lines that have the @whiteout-partial decorator
37            if !DECORATOR_PATTERN.is_match(line) {
38                continue;
39            }
40            
41            let mut replacements = Vec::new();
42            
43            for capture in PATTERN.captures_iter(line) {
44                let match_pos = capture.get(0).unwrap();
45                let local_value = capture.get(1).unwrap().as_str().to_string();
46                let committed_value = capture.get(2).unwrap().as_str().to_string();
47                
48                replacements.push(PartialReplacement {
49                    start: match_pos.start(),
50                    end: match_pos.end(),
51                    local_value,
52                    committed_value,
53                });
54            }
55            
56            if !replacements.is_empty() {
57                decorations.push(Decoration::Partial {
58                    line: line_num + 1,
59                    replacements,
60                });
61            }
62        }
63        
64        Ok(decorations)
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_partial_parser_with_decorator() {
74        let parser = PartialParser::new();
75        let content = r#"let url = "https://[[localhost:8080||api.example.com]]/v1"; // @whiteout-partial"#;
76        
77        let decorations = parser.parse(content).unwrap();
78        assert_eq!(decorations.len(), 1);
79        
80        match &decorations[0] {
81            Decoration::Partial { line, replacements } => {
82                assert_eq!(*line, 1);
83                assert_eq!(replacements.len(), 1);
84                assert_eq!(replacements[0].local_value, "localhost:8080");
85                assert_eq!(replacements[0].committed_value, "api.example.com");
86            }
87            _ => panic!("Expected partial decoration"),
88        }
89    }
90    
91    #[test]
92    fn test_partial_parser_without_decorator_ignores() {
93        let parser = PartialParser::new();
94        // This should be ignored because it lacks @whiteout-partial
95        let content = r#"let url = "https://[[localhost:8080||api.example.com]]/v1";"#;
96        
97        let decorations = parser.parse(content).unwrap();
98        assert_eq!(decorations.len(), 0);
99    }
100
101    #[test]
102    fn test_multiple_partial_replacements_with_decorator() {
103        let parser = PartialParser::new();
104        let content = r#"let config = { host: "[[dev.local||prod.com]]", port: [[8080||443]] }; // @whiteout-partial"#;
105        
106        let decorations = parser.parse(content).unwrap();
107        assert_eq!(decorations.len(), 1);
108        
109        match &decorations[0] {
110            Decoration::Partial { line, replacements } => {
111                assert_eq!(*line, 1);
112                assert_eq!(replacements.len(), 2);
113            }
114            _ => panic!("Expected partial decoration"),
115        }
116    }
117    
118    #[test]
119    fn test_safe_from_accidental_matches() {
120        let parser = PartialParser::new();
121        // These should all be ignored without the decorator
122        let content = r#"
123// Markdown table: | Column [[A||B]] | Description |
124let matrix = data[[row||col]];  // Array notation
125let pattern = "[[a-z]||[0-9]]"; // Regex pattern
126"#;
127        
128        let decorations = parser.parse(content).unwrap();
129        assert_eq!(decorations.len(), 0);
130    }
131}