whiteout/parser/
mod.rs

1pub mod apply;
2pub mod block;
3pub mod inline;
4pub mod partial;
5pub mod simple;
6pub mod types;
7
8use anyhow::Result;
9pub use types::{Decoration, PartialReplacement};
10
11pub struct Parser {
12    inline_parser: inline::InlineParser,
13    block_parser: block::BlockParser,
14    partial_parser: partial::PartialParser,
15    simple_parser: simple::SimpleParser,
16}
17
18impl Parser {
19    pub fn new() -> Self {
20        Self {
21            inline_parser: inline::InlineParser::new(),
22            block_parser: block::BlockParser::new(),
23            partial_parser: partial::PartialParser::new(),
24            simple_parser: simple::SimpleParser::new(),
25        }
26    }
27
28    pub fn parse(&self, content: &str) -> Result<Vec<Decoration>> {
29        let mut decorations = Vec::new();
30        
31        // Parse simple @whiteout decorations first
32        decorations.extend(self.simple_parser.parse(content)?);
33        
34        // Parse inline decorations
35        decorations.extend(self.inline_parser.parse(content)?);
36        
37        // Parse block decorations
38        decorations.extend(self.block_parser.parse(content)?);
39        
40        // Parse partial replacements
41        decorations.extend(self.partial_parser.parse(content)?);
42        
43        Ok(decorations)
44    }
45
46    pub fn apply_decorations(
47        &self,
48        content: &str,
49        decorations: &[Decoration],
50        use_local: bool,
51    ) -> String {
52        apply::apply_decorations(content, decorations, use_local)
53    }
54}
55
56impl Default for Parser {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_parse_inline() -> Result<()> {
68        let parser = Parser::new();
69        let content = r#"let api_key = "sk-12345"; // @whiteout: "ENV_VAR""#;
70        let decorations = parser.parse(content)?;
71        
72        assert_eq!(decorations.len(), 1);
73        match &decorations[0] {
74            Decoration::Inline { local_value, committed_value, .. } => {
75                assert!(local_value.contains("sk-12345"));
76                assert_eq!(committed_value, "\"ENV_VAR\"");
77            }
78            _ => panic!("Expected inline decoration"),
79        }
80        
81        Ok(())
82    }
83
84    #[test]
85    fn test_parse_block() -> Result<()> {
86        let parser = Parser::new();
87        let content = r#"
88// @whiteout-start
89const DEBUG = true;
90// @whiteout-end
91const DEBUG = false;"#;
92        
93        let decorations = parser.parse(content)?;
94        assert_eq!(decorations.len(), 1);
95        
96        match &decorations[0] {
97            Decoration::Block { local_content, committed_content, .. } => {
98                assert!(local_content.contains("true"));
99                assert!(committed_content.contains("false"));
100            }
101            _ => panic!("Expected block decoration"),
102        }
103        
104        Ok(())
105    }
106
107    #[test]
108    fn test_apply_decorations_clean() -> Result<()> {
109        let parser = Parser::new();
110        
111        // Test inline decoration - preserves marker for smudge
112        let content = r#"let api_key = "sk-12345"; // @whiteout: "REDACTED""#;
113        let decorations = parser.parse(content)?;
114        let cleaned = parser.apply_decorations(content, &decorations, false);
115        assert_eq!(cleaned, "\"REDACTED\" // @whiteout: \"REDACTED\"");
116        assert!(cleaned.contains("@whiteout"));  // Marker is preserved
117        assert!(!cleaned.contains("sk-12345"));  // Secret is removed
118        
119        // Test block decoration - preserves markers for smudge
120        let content = r#"code before
121// @whiteout-start
122const DEBUG = true;
123// @whiteout-end
124const DEBUG = false;
125code after"#;
126        let decorations = parser.parse(content)?;
127        let cleaned = parser.apply_decorations(content, &decorations, false);
128        assert!(cleaned.contains("code before"));
129        assert!(cleaned.contains("// @whiteout-start"));  // Marker preserved
130        assert!(cleaned.contains("// @whiteout-end"));    // Marker preserved  
131        assert!(cleaned.contains("const DEBUG = false;"));
132        assert!(cleaned.contains("code after"));
133        assert!(!cleaned.contains("const DEBUG = true;"));  // Local content removed
134        
135        Ok(())
136    }
137
138    #[test]
139    fn test_apply_decorations_smudge() -> Result<()> {
140        let parser = Parser::new();
141        
142        // Test inline decoration preservation
143        let content = r#""REDACTED" // @whiteout: "REDACTED""#;
144        let decorations = vec![Decoration::Inline {
145            line: 1,
146            local_value: r#"let api_key = "sk-12345";"#.to_string(),
147            committed_value: "\"REDACTED\"".to_string(),
148        }];
149        let smudged = parser.apply_decorations(content, &decorations, true);
150        assert!(smudged.contains("sk-12345"));
151        assert!(smudged.contains("@whiteout"));
152        
153        Ok(())
154    }
155    
156    #[test]
157    fn test_incomplete_block() -> Result<()> {
158        let parser = Parser::new();
159        
160        // Test that incomplete blocks are left as-is
161        let content = r#"
162// @whiteout-start
163const SECRET = "value";
164// Missing @whiteout-end
165const OTHER = "data";
166"#;
167        let decorations = parser.parse(content)?;
168        // Should not find any decorations since block is incomplete
169        if !decorations.is_empty() {
170            eprintln!("Found {} decorations:", decorations.len());
171            for (i, dec) in decorations.iter().enumerate() {
172                eprintln!("  {}: {:?}", i, dec);
173            }
174        }
175        assert_eq!(decorations.len(), 0);
176        
177        // When no decorations, apply_decorations should return content unchanged
178        let result = parser.apply_decorations(content, &decorations, false);
179        assert_eq!(result, content);
180        
181        Ok(())
182    }
183}