1use anyhow::Result;
2use once_cell::sync::Lazy;
3use regex::Regex;
4
5use super::Decoration;
6
7static START_PATTERN: Lazy<Regex> = Lazy::new(|| {
9 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 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 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 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 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 decorations.push(Decoration::Block {
79 start_line,
80 end_line: start_line + local_lines.len() + 1, 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 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}