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