1use std::path::Path;
2
3pub fn upsert(path: &Path, start: &str, end: &str, block: &str, quiet: bool, label: &str) {
4 let existing = std::fs::read_to_string(path).unwrap_or_default();
5
6 if existing.contains(start) {
7 let cleaned = remove_content(&existing, start, end);
8 let mut out = cleaned.trim_end().to_string();
9 if !out.is_empty() {
10 out.push('\n');
11 }
12 out.push('\n');
13 out.push_str(block);
14 out.push('\n');
15 std::fs::write(path, &out).ok();
16 if !quiet {
17 println!(" Updated {label}");
18 }
19 } else {
20 let mut out = existing;
21 if !out.is_empty() && !out.ends_with('\n') {
22 out.push('\n');
23 }
24 if !out.is_empty() {
25 out.push('\n');
26 }
27 out.push_str(block);
28 out.push('\n');
29 std::fs::write(path, &out).ok();
30 if !quiet {
31 eprintln!(" Installed {label}");
32 }
33 }
34}
35
36pub fn remove_from_file(path: &Path, start: &str, end: &str, quiet: bool, label: &str) {
37 let Ok(existing) = std::fs::read_to_string(path) else {
38 return;
39 };
40 if !existing.contains(start) {
41 return;
42 }
43 let cleaned = remove_content(&existing, start, end);
44 std::fs::write(path, cleaned.trim_end().to_owned() + "\n").ok();
45 if !quiet {
46 println!(" Removed {label}");
47 }
48}
49
50pub fn remove_content(content: &str, start: &str, end: &str) -> String {
51 let s = content.find(start);
52 let e = content.find(end);
53 match (s, e) {
54 (Some(si), Some(ei)) if ei >= si => {
55 let after_end = ei + end.len();
56 let before = content[..si].trim_end_matches('\n');
57 let after = content[after_end..].trim_start_matches('\n');
58 let mut out = before.to_string();
59 if !after.is_empty() {
60 out.push('\n');
61 out.push_str(after);
62 }
63 out
64 }
65 _ => content.to_string(),
66 }
67}
68
69pub fn replace_marked_block(content: &str, start: &str, end: &str, replacement: &str) -> String {
72 let s = content.find(start);
73 let e = content.find(end);
74 match (s, e) {
75 (Some(si), Some(ei)) if ei >= si => {
76 let after_end = ei + end.len();
77 let before = &content[..si];
78 let after = &content[after_end..];
79 let mut out = String::new();
80 out.push_str(before.trim_end_matches('\n'));
81 out.push('\n');
82 out.push('\n');
83 out.push_str(replacement.trim_end_matches('\n'));
84 out.push('\n');
85 out.push_str(after.trim_start_matches('\n'));
86 out
87 }
88 _ => content.to_string(),
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn remove_content_works() {
98 let content = "before\n# >>> start >>>\nhook content\n# <<< end <<<\nafter\n";
99 let cleaned = remove_content(content, "# >>> start >>>", "# <<< end <<<");
100 assert!(!cleaned.contains("hook content"));
101 assert!(cleaned.contains("before"));
102 assert!(cleaned.contains("after"));
103 }
104
105 #[test]
106 fn remove_content_preserves_when_missing() {
107 let content = "no hook here\n";
108 let cleaned = remove_content(content, "# >>> start >>>", "# <<< end <<<");
109 assert_eq!(cleaned, content);
110 }
111}