1use std::fmt::{Display, Formatter, Result as FmtResult};
2
3#[derive(Clone, Debug, Default, Eq, PartialEq)]
8pub struct Note {
9 lines: Vec<String>,
10}
11
12impl Note {
13 pub fn from_lines(lines: impl IntoIterator<Item = impl Into<String>>) -> Self {
15 Self {
16 lines: lines.into_iter().map(Into::into).collect(),
17 }
18 }
19
20 #[allow(clippy::should_implement_trait)]
22 pub fn from_str(text: &str) -> Self {
23 Self {
24 lines: text.lines().map(String::from).collect(),
25 }
26 }
27
28 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn add(&mut self, text: impl Into<String>) {
35 let text = text.into();
36 for line in text.lines() {
37 self.lines.push(line.to_string());
38 }
39 }
40
41 pub fn compress(&mut self) {
45 for line in &mut self.lines {
47 let trimmed = line.trim_end().to_string();
48 *line = trimmed;
49 }
50
51 let mut compressed = Vec::new();
53 let mut prev_blank = false;
54 for line in &self.lines {
55 let is_blank = line.trim().is_empty();
56 if is_blank {
57 if !prev_blank {
58 compressed.push(String::new());
59 }
60 prev_blank = true;
61 } else {
62 compressed.push(line.clone());
63 prev_blank = false;
64 }
65 }
66 self.lines = compressed;
67
68 while self.lines.first().is_some_and(|l| l.trim().is_empty()) {
70 self.lines.remove(0);
71 }
72 while self.lines.last().is_some_and(|l| l.trim().is_empty()) {
73 self.lines.pop();
74 }
75 }
76
77 pub fn is_empty(&self) -> bool {
79 self.lines.is_empty() || self.lines.iter().all(|l| l.trim().is_empty())
80 }
81
82 #[allow(dead_code)]
84 pub fn len(&self) -> usize {
85 self.lines.len()
86 }
87
88 pub fn lines(&self) -> &[String] {
90 &self.lines
91 }
92
93 pub fn to_line(&self, separator: &str) -> String {
95 let lines: Vec<&str> = self.compressed_lines().collect();
96 lines.join(separator)
97 }
98
99 fn compressed_lines(&self) -> impl Iterator<Item = &str> {
101 let mut prev_blank = true; let mut lines: Vec<&str> = Vec::new();
103 for line in &self.lines {
104 let trimmed = line.trim_end();
105 let is_blank = trimmed.trim().is_empty();
106 if is_blank {
107 if !prev_blank {
108 lines.push("");
109 }
110 prev_blank = true;
111 } else {
112 lines.push(trimmed);
113 prev_blank = false;
114 }
115 }
116 while lines.last().is_some_and(|l| l.trim().is_empty()) {
118 lines.pop();
119 }
120 lines.into_iter()
121 }
122}
123
124impl Display for Note {
125 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
127 for (i, line) in self.compressed_lines().enumerate() {
128 if i > 0 {
129 writeln!(f)?;
130 }
131 write!(f, "\t\t{line}")?;
132 }
133 Ok(())
134 }
135}
136
137#[cfg(test)]
138mod test {
139 use super::*;
140
141 mod compress {
142 use pretty_assertions::assert_eq;
143
144 use super::*;
145
146 #[test]
147 fn it_collapses_consecutive_blank_lines() {
148 let mut note = Note::from_lines(vec!["first", "", "", "", "second"]);
149
150 note.compress();
151
152 assert_eq!(note.lines(), &["first", "", "second"]);
153 }
154
155 #[test]
156 fn it_removes_leading_blank_lines() {
157 let mut note = Note::from_lines(vec!["", "", "content"]);
158
159 note.compress();
160
161 assert_eq!(note.lines(), &["content"]);
162 }
163
164 #[test]
165 fn it_removes_trailing_blank_lines() {
166 let mut note = Note::from_lines(vec!["content", "", ""]);
167
168 note.compress();
169
170 assert_eq!(note.lines(), &["content"]);
171 }
172
173 #[test]
174 fn it_trims_trailing_whitespace_from_lines() {
175 let mut note = Note::from_lines(vec!["hello ", "world "]);
176
177 note.compress();
178
179 assert_eq!(note.lines(), &["hello", "world"]);
180 }
181 }
182
183 mod display {
184 use pretty_assertions::assert_eq;
185
186 use super::*;
187
188 #[test]
189 fn it_formats_with_tab_prefix() {
190 let note = Note::from_lines(vec!["line one", "line two"]);
191
192 assert_eq!(note.to_string(), "\t\tline one\n\t\tline two");
193 }
194 }
195
196 mod from_str {
197 use pretty_assertions::assert_eq;
198
199 use super::*;
200
201 #[test]
202 fn it_splits_on_newlines() {
203 let note = Note::from_str("line one\nline two\nline three");
204
205 assert_eq!(note.lines(), &["line one", "line two", "line three"]);
206 }
207 }
208
209 mod is_empty {
210 use super::*;
211
212 #[test]
213 fn it_returns_true_for_empty_note() {
214 let note = Note::new();
215
216 assert!(note.is_empty());
217 }
218
219 #[test]
220 fn it_returns_true_for_blank_lines_only() {
221 let note = Note::from_lines(vec!["", " ", "\t"]);
222
223 assert!(note.is_empty());
224 }
225
226 #[test]
227 fn it_returns_false_for_content() {
228 let note = Note::from_lines(vec!["hello"]);
229
230 assert!(!note.is_empty());
231 }
232 }
233
234 mod to_line {
235 use pretty_assertions::assert_eq;
236
237 use super::*;
238
239 #[test]
240 fn it_joins_with_separator() {
241 let note = Note::from_lines(vec!["one", "two", "three"]);
242
243 assert_eq!(note.to_line(" "), "one two three");
244 }
245
246 #[test]
247 fn it_compresses_before_joining() {
248 let note = Note::from_lines(vec!["", "one", "", "", "two", ""]);
249
250 assert_eq!(note.to_line("|"), "one||two");
251 }
252 }
253}