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