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