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;]*[A-Za-z]").unwrap());
8
9pub fn serialize(doc: &Document) -> String {
14 let mut doc = doc.clone();
15 doc.dedup();
16 strip_ansi(&doc.to_string())
17}
18
19fn 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}