1use chrono::{DateTime, Duration, Local, TimeZone};
2
3const SIGIL: &str = "- [";
4
5fn now() -> DateTime<Local> {
6 Local::now()
7}
8
9fn mdbook_summary_line_for_time<T: TimeZone>(dt: DateTime<T>) -> String
10where
11 T::Offset: std::fmt::Display,
12{
13 dt.format("- [%A, %b %d, %Y](./%Y/%Y-%m/%Y-%m-%d.md)")
14 .to_string()
15}
16
17fn todays_line() -> String {
18 mdbook_summary_line_for_time(now())
19}
20
21fn future_lines(days: u32) -> Vec<String> {
22 let today = now();
23 (1..=days)
24 .map(|day_offset| {
25 let future_date = today + Duration::days(day_offset as i64);
26 mdbook_summary_line_for_time(future_date)
27 })
28 .collect::<Vec<_>>()
29 .into_iter()
30 .rev()
31 .collect()
32}
33
34fn insert_lines_before_sigil(lines: &[String], sigil: &str, text: &str) -> String {
38 let mut new_lines = vec![];
39 let mut sigil_found = false;
40 let mut already_added_lines: std::collections::HashSet<String> =
41 std::collections::HashSet::new();
42
43 for text_line in text.lines() {
45 for line in lines {
46 if text_line == line {
47 already_added_lines.insert(line.clone());
48 }
49 }
50 }
51
52 for text_line in text.lines() {
54 if text_line.starts_with(sigil) && !sigil_found {
55 for line in lines {
57 if !already_added_lines.contains(line) {
58 new_lines.push(line.as_str());
59 already_added_lines.insert(line.clone());
60 }
61 }
62 sigil_found = true;
63 }
64 new_lines.push(text_line);
65 }
66
67 if !sigil_found {
69 for line in lines {
70 if !already_added_lines.contains(line) {
71 new_lines.push(line.as_str());
72 }
73 }
74 }
75
76 let mut with_insert = new_lines.join("\n");
77 with_insert.push('\n');
78 with_insert
79}
80
81fn add_lines_to_file(lines: &[String], sigil: &str, file_path: &str) -> Result<(), std::io::Error> {
82 let file_contents = std::fs::read_to_string(file_path)?;
83 let file_contents = insert_lines_before_sigil(lines, sigil, &file_contents);
84 std::fs::write(file_path, file_contents)
85}
86
87fn add_line_to_file(line: &str, sigil: &str, file_path: &str) -> Result<(), std::io::Error> {
88 add_lines_to_file(&[line.to_string()], sigil, file_path)
89}
90
91#[cfg(test)]
93fn insert_line_before_sigil(line: &str, sigil: &str, text: &str) -> String {
94 insert_lines_before_sigil(&[line.to_string()], sigil, text)
95}
96
97pub fn update_summary(path: &str) -> Result<(), std::io::Error> {
110 add_line_to_file(&todays_line(), SIGIL, path)
111}
112
113pub fn update_summary_plan_ahead(path: &str, days: u32) -> Result<(), std::io::Error> {
121 let lines = future_lines(days);
122 add_lines_to_file(&lines, SIGIL, path)
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use chrono::Utc;
129
130 #[test]
131 fn test_update_summary() -> Result<(), std::io::Error> {
132 let tmp_path = "./tmp_file";
133 let original_line = "original_line";
134
135 std::fs::write(tmp_path, original_line)?;
137
138 update_summary(tmp_path)?;
140
141 let added_line = &todays_line();
143 let contents = std::fs::read_to_string(tmp_path)?;
144 assert_eq!(contents, format!("{}\n{}\n", original_line, added_line));
145
146 std::fs::remove_file(tmp_path)?;
148 Ok(())
149 }
150
151 #[test]
152 fn test_place_line_before() {
153 let line = "- [First!](./first.md)";
154 let line_without_sigil = "no sigil";
155 let sigil = "- [";
156 let text_without_sigil = "[Intro](./intro)";
157 let text_with_sigil = "[Intro](./intro)\n- [Second!](./second.md)";
158 let text_with_2_sigils = "[Intro](./intro)\n- [\n- [";
159
160 let text = insert_line_before_sigil(line, sigil, text_without_sigil);
162 assert_eq!(text, "[Intro](./intro)\n- [First!](./first.md)\n");
163
164 let text = insert_line_before_sigil(line, sigil, text_with_sigil);
166 assert_eq!(
167 text,
168 "[Intro](./intro)\n- [First!](./first.md)\n- [Second!](./second.md)\n"
169 );
170
171 let text = insert_line_before_sigil(line, sigil, &text);
173 assert_eq!(
174 text,
175 "[Intro](./intro)\n- [First!](./first.md)\n- [Second!](./second.md)\n"
176 );
177
178 let text = insert_line_before_sigil(line, sigil, text_with_2_sigils);
180 assert_eq!(text, "[Intro](./intro)\n- [First!](./first.md)\n- [\n- [\n");
181
182 let first_pass = insert_line_before_sigil(line_without_sigil, sigil, text_without_sigil);
184 assert_eq!(first_pass, "[Intro](./intro)\nno sigil\n");
185 let second_pass = insert_line_before_sigil(line_without_sigil, sigil, &first_pass);
186 assert_eq!(first_pass, second_pass)
187 }
188
189 #[test]
190 fn test_gives_summary_terminal_newline() {
191 let sigil = "- [";
192 let without_sigil = "[Introduction](introduction.md)";
193 let with_sigil = "- [a](a.md)";
194 let line = "- [Thursday, Jan 01, 1970](./1970/1970-01/1970-01-01.md)";
195
196 let expected = format!("{without_sigil}\n{line}\n{with_sigil}\n");
197
198 let text = format!("{without_sigil}\n{with_sigil}\n");
200 let with_insert = insert_line_before_sigil(line, sigil, &text);
201 assert_eq!(with_insert, expected);
202
203 let text = format!("{without_sigil}\n{with_sigil}");
205 let with_insert = insert_line_before_sigil(line, sigil, &text);
206 assert_eq!(with_insert, expected);
207 }
208
209 #[test]
210 fn test_summary_line_format() {
211 let dt: DateTime<Utc> = Utc.timestamp_opt(1, 0).unwrap();
212 let formatted = mdbook_summary_line_for_time(dt);
213 assert_eq!(
214 formatted,
215 "- [Thursday, Jan 01, 1970](./1970/1970-01/1970-01-01.md)"
216 );
217 }
218
219 #[test]
220 fn test_todays_line() {
221 dbg!(todays_line());
224 }
225 #[test]
226 fn test_lines() {
227 let lines = "a\n";
228 for line in lines.lines() {
229 println!("Here's the line: {line:?}")
230 }
231 }
232
233 #[test]
234 fn test_future_lines() {
235 let lines = future_lines(3);
237 assert_eq!(lines.len(), 3);
238
239 for line in &lines {
241 assert!(line.starts_with("- ["));
242 assert!(line.contains("](./"));
243 assert!(line.ends_with(".md)"));
244 }
245
246 assert!(lines.len() >= 2);
250 }
251
252 #[test]
253 fn test_insert_multiple_lines() {
254 let lines = vec".to_string(),
256 "- [Day 2](./day2.md)".to_string(),
257 "- [Day 3](./day3.md)".to_string(),
258 ];
259 let sigil = "- [";
260 let text_with_sigil = "[Intro](./intro)\n- [Existing](./existing.md)";
261
262 let result = insert_lines_before_sigil(&lines, sigil, text_with_sigil);
263 let expected = "[Intro](./intro)\n- [Day 1](./day1.md)\n- [Day 2](./day2.md)\n- [Day 3](./day3.md)\n- [Existing](./existing.md)\n";
264 assert_eq!(result, expected);
265 }
266
267 #[test]
268 fn test_insert_multiple_lines_no_duplicates() {
269 let lines = vec".to_string(),
271 "- [Day 2](./day2.md)".to_string(),
272 ];
273 let sigil = "- [";
274 let text_with_existing =
276 "[Intro](./intro)\n- [Day 1](./day1.md)\n- [Existing](./existing.md)";
277
278 let result = insert_lines_before_sigil(&lines, sigil, text_with_existing);
279 let expected = "[Intro](./intro)\n- [Day 2](./day2.md)\n- [Day 1](./day1.md)\n- [Existing](./existing.md)\n";
280 assert_eq!(result, expected);
281 }
282
283 #[test]
284 fn test_update_summary_plan_ahead() -> Result<(), std::io::Error> {
285 let tmp_path = "./tmp_plan_ahead_file";
286 let original_content = "[Introduction](intro.md)\n- [Existing Entry](./existing.md)\n";
287
288 std::fs::write(tmp_path, original_content)?;
290
291 update_summary_plan_ahead(tmp_path, 2)?;
293
294 let contents = std::fs::read_to_string(tmp_path)?;
296
297 let lines: Vec<&str> = contents.lines().collect();
299 assert_eq!(lines.len(), 4); assert_eq!(lines[0], "[Introduction](intro.md)");
301 assert!(lines[1].starts_with("- ["));
302 assert!(lines[2].starts_with("- ["));
303 assert_eq!(lines[3], "- [Existing Entry](./existing.md)");
304
305 std::fs::remove_file(tmp_path)?;
307 Ok(())
308 }
309}