whiteout/parser/
partial.rs1use anyhow::Result;
2use once_cell::sync::Lazy;
3use regex::Regex;
4
5use super::{Decoration, PartialReplacement};
6
7static 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 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 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 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 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}