1pub struct Entry<'a> {
3 pub date: &'a str, pub time: &'a str, pub raw: Option<&'a str>, pub clean: &'a str,
7}
8
9#[derive(Clone, Copy, PartialEq, Eq)]
13pub enum Mode { Reflect, Journal }
14
15pub fn append(body: &str, entry: &Entry, mode: Mode) -> String {
19 let block = render_turn(entry);
20 match mode {
21 Mode::Journal => join(body, &format!("\n## {}\n{}", entry.time, block)),
22 Mode::Reflect => {
23 let date_header = format!("## {}", entry.date);
24 if body.contains(&date_header) {
25 join(body, &format!("\n### {}\n{}", entry.time, block))
26 } else {
27 join(body, &format!("\n{}\n{}", date_header, block))
28 }
29 }
30 }
31}
32
33fn render_turn(entry: &Entry) -> String {
34 match entry.raw {
35 Some(raw) => format!("<!-- raw: {} -->\n{}\n", sanitize_comment(raw), entry.clean),
36 None => format!("{}\n", entry.clean),
37 }
38}
39
40fn sanitize_comment(raw: &str) -> String {
43 raw.replace('<', "<")
44 .replace("--", "--")
45 .replace('\n', " ")
46}
47
48fn join(body: &str, section: &str) -> String {
49 format!("{}\n{}", body.trim_end_matches('\n'), section.trim_start_matches('\n'))
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 fn turn<'a>(date: &'a str, time: &'a str, raw: Option<&'a str>, clean: &'a str) -> Entry<'a> {
57 Entry { date, time, raw, clean }
58 }
59
60 #[test]
61 fn reflect_new_date_appends_a_dated_section() {
62 let out = append("", &turn("2026-06-06", "08:14", Some("um the thing is"), "The thing is."), Mode::Reflect);
63 assert!(out.contains("## 2026-06-06"));
64 assert!(out.contains("<!-- raw: um the thing is -->"));
65 assert!(out.contains("The thing is."));
66 }
67
68 #[test]
69 fn reflect_repeat_date_nests_a_timestamped_subsection() {
70 let body = "\n## 2026-06-06\nFirst.\n";
71 let out = append(body, &turn("2026-06-06", "20:15", None, "Second."), Mode::Reflect);
72 assert!(out.contains("### 20:15"));
73 assert_eq!(out.matches("## 2026-06-06").count(), 1);
74 }
75
76 #[test]
77 fn journal_sections_are_time_keyed() {
78 let out = append("", &turn("2026-06-08", "08:14", None, "Morning."), Mode::Journal);
79 assert!(out.contains("## 08:14") && !out.contains("## 2026-06-08"));
80 let out2 = append(&out, &turn("2026-06-08", "21:30", None, "Night."), Mode::Journal);
81 assert!(out2.contains("## 08:14") && out2.contains("## 21:30"));
82 }
83
84 #[test]
85 fn raw_none_omits_the_comment() {
86 let out = append("", &turn("2026-06-06", "08:14", None, "Clean only."), Mode::Reflect);
87 assert!(!out.contains("<!-- raw"));
88 }
89
90 #[test]
91 fn comment_breakout_chars_are_neutralized() {
92 let out = append("", &turn("2026-06-06", "08:14", Some("end --> <!-- x"), "y"), Mode::Reflect);
93 assert_eq!(out.matches("-->").count(), 1); assert!(!out.contains("<!-- x"));
95 }
96}