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 decorations.extend(self.simple_parser.parse(content)?);
33
34 decorations.extend(self.inline_parser.parse(content)?);
36
37 decorations.extend(self.block_parser.parse(content)?);
39
40 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 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")); assert!(!cleaned.contains("sk-12345")); 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")); assert!(cleaned.contains("// @whiteout-end")); assert!(cleaned.contains("const DEBUG = false;"));
132 assert!(cleaned.contains("code after"));
133 assert!(!cleaned.contains("const DEBUG = true;")); Ok(())
136 }
137
138 #[test]
139 fn test_apply_decorations_smudge() -> Result<()> {
140 let parser = Parser::new();
141
142 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 let content = r#"
162// @whiteout-start
163const SECRET = "value";
164// Missing @whiteout-end
165const OTHER = "data";
166"#;
167 let decorations = parser.parse(content)?;
168 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 let result = parser.apply_decorations(content, &decorations, false);
179 assert_eq!(result, content);
180
181 Ok(())
182 }
183}