Skip to main content

doing_taskpaper/
serializer.rs

1use std::sync::LazyLock;
2
3use regex::Regex;
4
5use crate::Document;
6
7static STRIP_ANSI_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\x1b\[[0-9;]*[A-Za-z]").unwrap());
8
9/// Serialize a `Document` into the doing file format string.
10///
11/// Deduplicates entries by ID and strips any ANSI color codes from the output.
12/// Callers are responsible for sorting entries before calling this function.
13pub fn serialize(doc: &Document) -> String {
14  let mut doc = doc.clone();
15  doc.dedup();
16  strip_ansi(&doc.to_string())
17}
18
19/// Remove ANSI escape sequences from a string.
20fn strip_ansi(text: &str) -> String {
21  STRIP_ANSI_RE.replace_all(text, "").into_owned()
22}
23
24#[cfg(test)]
25mod test {
26  use chrono::{Local, TimeZone};
27
28  use super::*;
29  use crate::{Entry, Note, Section, Tag, Tags};
30
31  fn sample_date(hour: u32, minute: u32) -> chrono::DateTime<Local> {
32    Local.with_ymd_and_hms(2024, 3, 17, hour, minute, 0).unwrap()
33  }
34
35  mod serialize {
36    use pretty_assertions::assert_eq;
37
38    use super::*;
39
40    #[test]
41    fn it_produces_empty_string_for_empty_document() {
42      let doc = Document::new();
43
44      assert_eq!(serialize(&doc), "");
45    }
46
47    #[test]
48    fn it_round_trips_a_well_formed_document() {
49      let content = "\
50Currently:
51\t- 2024-03-17 14:30 | Working on feature @coding <aaaabbbbccccddddeeeeffffaaaabbbb>
52\t\tA note about the work
53Archive:
54\t- 2024-03-16 10:00 | Old task @done(2024-03-16 11:00) <bbbbccccddddeeeeffffaaaabbbbcccc>";
55      let doc = Document::parse(content);
56
57      let output = serialize(&doc);
58
59      assert_eq!(output, content);
60    }
61
62    #[test]
63    fn it_deduplicates_entries_by_id() {
64      let mut doc = Document::new();
65      let entry = Entry::new(
66        sample_date(14, 30),
67        "Task A",
68        Tags::new(),
69        Note::new(),
70        "Currently",
71        Some("aaaabbbbccccddddeeeeffffaaaabbbb"),
72      );
73      let mut s1 = Section::new("Currently");
74      s1.add_entry(entry.clone());
75      let mut s2 = Section::new("Archive");
76      s2.add_entry(entry);
77      doc.add_section(s1);
78      doc.add_section(s2);
79
80      let output = serialize(&doc);
81
82      assert_eq!(output.matches("Task A").count(), 1);
83    }
84
85    #[test]
86    fn it_strips_ansi_color_codes() {
87      let mut doc = Document::new();
88      let mut section = Section::new("Currently");
89      section.add_entry(Entry::new(
90        sample_date(14, 30),
91        "\x1b[31mRed task\x1b[0m",
92        Tags::new(),
93        Note::new(),
94        "Currently",
95        Some("aaaabbbbccccddddeeeeffffaaaabbbb"),
96      ));
97      doc.add_section(section);
98
99      let output = serialize(&doc);
100
101      assert!(!output.contains("\x1b["));
102      assert!(output.contains("Red task"));
103    }
104
105    #[test]
106    fn it_preserves_other_content_top() {
107      let mut doc = Document::new();
108      doc.other_content_top_mut().push("# My Doing File".to_string());
109      doc.add_section(Section::new("Currently"));
110
111      let output = serialize(&doc);
112
113      assert!(output.starts_with("# My Doing File\n"));
114    }
115
116    #[test]
117    fn it_preserves_other_content_bottom() {
118      let mut doc = Document::new();
119      doc.add_section(Section::new("Currently"));
120      doc.other_content_bottom_mut().push("# Footer".to_string());
121
122      let output = serialize(&doc);
123
124      assert!(output.ends_with("# Footer"));
125    }
126
127    #[test]
128    fn it_includes_notes() {
129      let mut doc = Document::new();
130      let mut section = Section::new("Currently");
131      section.add_entry(Entry::new(
132        sample_date(14, 30),
133        "Task with notes",
134        Tags::new(),
135        Note::from_text("A note line\nAnother note"),
136        "Currently",
137        Some("aaaabbbbccccddddeeeeffffaaaabbbb"),
138      ));
139      doc.add_section(section);
140
141      let output = serialize(&doc);
142
143      assert!(output.contains("\t\tA note line"));
144      assert!(output.contains("\t\tAnother note"));
145    }
146
147    #[test]
148    fn it_includes_tags() {
149      let mut doc = Document::new();
150      let mut section = Section::new("Currently");
151      section.add_entry(Entry::new(
152        sample_date(14, 30),
153        "Tagged task",
154        Tags::from_iter(vec![
155          Tag::new("coding", None::<String>),
156          Tag::new("done", Some("2024-03-17 15:00")),
157        ]),
158        Note::new(),
159        "Currently",
160        Some("aaaabbbbccccddddeeeeffffaaaabbbb"),
161      ));
162      doc.add_section(section);
163
164      let output = serialize(&doc);
165
166      assert!(output.contains("@coding"));
167      assert!(output.contains("@done(2024-03-17 15:00)"));
168    }
169  }
170
171  mod strip_ansi {
172    use pretty_assertions::assert_eq;
173
174    use super::*;
175
176    #[test]
177    fn it_removes_ansi_escape_sequences() {
178      let input = "\x1b[31mhello\x1b[0m world";
179
180      assert_eq!(strip_ansi(input), "hello world");
181    }
182
183    #[test]
184    fn it_returns_unchanged_string_without_ansi() {
185      let input = "hello world";
186
187      assert_eq!(strip_ansi(input), "hello world");
188    }
189  }
190}