Skip to main content

sal_text/
replace.rs

1use regex::Regex;
2use std::fs;
3use std::io::{self, Read};
4use std::path::Path;
5
6/// Represents the type of replacement to perform.
7#[derive(Clone)]
8pub enum ReplaceMode {
9    /// Regex-based replacement using the `regex` crate
10    Regex(Regex),
11    /// Literal substring replacement (non-regex)
12    Literal(String),
13}
14
15/// A single replacement operation with a pattern and replacement text
16#[derive(Clone)]
17pub struct ReplacementOperation {
18    mode: ReplaceMode,
19    replacement: String,
20}
21
22impl ReplacementOperation {
23    /// Applies this replacement operation to the input text
24    fn apply(&self, input: &str) -> String {
25        match &self.mode {
26            ReplaceMode::Regex(re) => re.replace_all(input, self.replacement.as_str()).to_string(),
27            ReplaceMode::Literal(search) => input.replace(search, &self.replacement),
28        }
29    }
30}
31
32/// Text replacer that can perform multiple replacement operations
33/// in a single pass over the input text.
34#[derive(Clone)]
35pub struct TextReplacer {
36    operations: Vec<ReplacementOperation>,
37}
38
39impl TextReplacer {
40    /// Creates a new builder for configuring a TextReplacer
41    pub fn builder() -> TextReplacerBuilder {
42        TextReplacerBuilder::default()
43    }
44
45    /// Applies all configured replacement operations to the input text
46    pub fn replace(&self, input: &str) -> String {
47        let mut result = input.to_string();
48
49        // Apply each replacement operation in sequence
50        for op in &self.operations {
51            result = op.apply(&result);
52        }
53
54        result
55    }
56
57    /// Reads a file, applies all replacements, and returns the result as a string
58    pub fn replace_file<P: AsRef<Path>>(&self, path: P) -> io::Result<String> {
59        let mut file = fs::File::open(path)?;
60        let mut content = String::new();
61        file.read_to_string(&mut content)?;
62
63        Ok(self.replace(&content))
64    }
65
66    /// Reads a file, applies all replacements, and writes the result back to the file
67    pub fn replace_file_in_place<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
68        let content = self.replace_file(&path)?;
69        fs::write(path, content)?;
70        Ok(())
71    }
72
73    /// Reads a file, applies all replacements, and writes the result to a new file
74    pub fn replace_file_to<P1: AsRef<Path>, P2: AsRef<Path>>(
75        &self,
76        input_path: P1,
77        output_path: P2,
78    ) -> io::Result<()> {
79        let content = self.replace_file(&input_path)?;
80        fs::write(output_path, content)?;
81        Ok(())
82    }
83}
84
85/// Builder for the TextReplacer.
86#[derive(Default, Clone)]
87pub struct TextReplacerBuilder {
88    operations: Vec<ReplacementOperation>,
89    pattern: Option<String>,
90    replacement: Option<String>,
91    use_regex: bool,
92    case_insensitive: bool,
93}
94
95impl TextReplacerBuilder {
96    /// Sets the pattern to search for
97    pub fn pattern(mut self, pat: &str) -> Self {
98        self.pattern = Some(pat.to_string());
99        self
100    }
101
102    /// Sets the replacement text
103    pub fn replacement(mut self, rep: &str) -> Self {
104        self.replacement = Some(rep.to_string());
105        self
106    }
107
108    /// Sets whether to use regex
109    pub fn regex(mut self, yes: bool) -> Self {
110        self.use_regex = yes;
111        self
112    }
113
114    /// Sets whether the replacement should be case-insensitive
115    pub fn case_insensitive(mut self, yes: bool) -> Self {
116        self.case_insensitive = yes;
117        self
118    }
119
120    /// Adds another replacement operation to the chain and resets the builder for a new operation
121    pub fn and(mut self) -> Self {
122        self.add_current_operation();
123        self
124    }
125
126    // Helper method to add the current operation to the list
127    fn add_current_operation(&mut self) -> bool {
128        if let Some(pattern) = self.pattern.take() {
129            let replacement = self.replacement.take().unwrap_or_default();
130            let use_regex = self.use_regex;
131            let case_insensitive = self.case_insensitive;
132
133            // Reset current settings
134            self.use_regex = false;
135            self.case_insensitive = false;
136
137            // Create the replacement mode
138            let mode = if use_regex {
139                let mut regex_pattern = pattern;
140
141                // If case insensitive, add the flag to the regex pattern
142                if case_insensitive && !regex_pattern.starts_with("(?i)") {
143                    regex_pattern = format!("(?i){}", regex_pattern);
144                }
145
146                match Regex::new(&regex_pattern) {
147                    Ok(re) => ReplaceMode::Regex(re),
148                    Err(_) => return false, // Failed to compile regex
149                }
150            } else {
151                // For literal replacement, we'll handle case insensitivity differently
152                // since String::replace doesn't have a case-insensitive option
153                if case_insensitive {
154                    return false; // Case insensitive not supported for literal
155                }
156                ReplaceMode::Literal(pattern)
157            };
158
159            self.operations
160                .push(ReplacementOperation { mode, replacement });
161
162            true
163        } else {
164            false
165        }
166    }
167
168    /// Builds the TextReplacer with all configured replacement operations
169    pub fn build(mut self) -> Result<TextReplacer, String> {
170        // If there's a pending replacement operation, add it
171        self.add_current_operation();
172
173        // Ensure we have at least one replacement operation
174        if self.operations.is_empty() {
175            return Err("No replacement operations configured".to_string());
176        }
177
178        Ok(TextReplacer {
179            operations: self.operations,
180        })
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use std::io::{Seek, SeekFrom, Write};
188    use tempfile::NamedTempFile;
189
190    #[test]
191    fn test_regex_replace() {
192        let replacer = TextReplacer::builder()
193            .pattern(r"\bfoo\b")
194            .replacement("bar")
195            .regex(true)
196            .build()
197            .unwrap();
198
199        let input = "foo bar foo baz";
200        let output = replacer.replace(input);
201
202        assert_eq!(output, "bar bar bar baz");
203    }
204
205    #[test]
206    fn test_literal_replace() {
207        let replacer = TextReplacer::builder()
208            .pattern("foo")
209            .replacement("qux")
210            .regex(false)
211            .build()
212            .unwrap();
213
214        let input = "foo bar foo baz";
215        let output = replacer.replace(input);
216
217        assert_eq!(output, "qux bar qux baz");
218    }
219
220    #[test]
221    fn test_multiple_replacements() {
222        let replacer = TextReplacer::builder()
223            .pattern("foo")
224            .replacement("qux")
225            .and()
226            .pattern("bar")
227            .replacement("baz")
228            .build()
229            .unwrap();
230
231        let input = "foo bar foo";
232        let output = replacer.replace(input);
233
234        assert_eq!(output, "qux baz qux");
235    }
236
237    #[test]
238    fn test_case_insensitive_regex() {
239        let replacer = TextReplacer::builder()
240            .pattern("foo")
241            .replacement("bar")
242            .regex(true)
243            .case_insensitive(true)
244            .build()
245            .unwrap();
246
247        let input = "FOO foo Foo";
248        let output = replacer.replace(input);
249
250        assert_eq!(output, "bar bar bar");
251    }
252
253    #[test]
254    fn test_file_operations() -> io::Result<()> {
255        // Create a temporary file
256        let mut temp_file = NamedTempFile::new()?;
257        writeln!(temp_file, "foo bar foo baz")?;
258
259        // Flush the file to ensure content is written
260        temp_file.as_file_mut().flush()?;
261
262        let replacer = TextReplacer::builder()
263            .pattern("foo")
264            .replacement("qux")
265            .build()
266            .unwrap();
267
268        // Test replace_file
269        let result = replacer.replace_file(temp_file.path())?;
270        assert_eq!(result, "qux bar qux baz\n");
271
272        // Test replace_file_in_place
273        replacer.replace_file_in_place(temp_file.path())?;
274
275        // Verify the file was updated - need to seek to beginning of file first
276        let mut content = String::new();
277        temp_file.as_file_mut().seek(SeekFrom::Start(0))?;
278        temp_file.as_file_mut().read_to_string(&mut content)?;
279        assert_eq!(content, "qux bar qux baz\n");
280
281        // Test replace_file_to with a new temporary file
282        let output_file = NamedTempFile::new()?;
283        replacer.replace_file_to(temp_file.path(), output_file.path())?;
284
285        // Verify the output file has the replaced content
286        let mut output_content = String::new();
287        fs::File::open(output_file.path())?.read_to_string(&mut output_content)?;
288        assert_eq!(output_content, "qux bar qux baz\n");
289
290        Ok(())
291    }
292}