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 mut note = self.clone();
96 note.compress();
97 note.lines.join(separator)
98 }
99}
100
101impl Display for Note {
102 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
104 let mut note = self.clone();
105 note.compress();
106 for (i, line) in note.lines.iter().enumerate() {
107 if i > 0 {
108 writeln!(f)?;
109 }
110 write!(f, "\t\t{line}")?;
111 }
112 Ok(())
113 }
114}
115
116#[cfg(test)]
117mod test {
118 use super::*;
119
120 mod compress {
121 use pretty_assertions::assert_eq;
122
123 use super::*;
124
125 #[test]
126 fn it_collapses_consecutive_blank_lines() {
127 let mut note = Note::from_lines(vec!["first", "", "", "", "second"]);
128
129 note.compress();
130
131 assert_eq!(note.lines(), &["first", "", "second"]);
132 }
133
134 #[test]
135 fn it_removes_leading_blank_lines() {
136 let mut note = Note::from_lines(vec!["", "", "content"]);
137
138 note.compress();
139
140 assert_eq!(note.lines(), &["content"]);
141 }
142
143 #[test]
144 fn it_removes_trailing_blank_lines() {
145 let mut note = Note::from_lines(vec!["content", "", ""]);
146
147 note.compress();
148
149 assert_eq!(note.lines(), &["content"]);
150 }
151
152 #[test]
153 fn it_trims_trailing_whitespace_from_lines() {
154 let mut note = Note::from_lines(vec!["hello ", "world "]);
155
156 note.compress();
157
158 assert_eq!(note.lines(), &["hello", "world"]);
159 }
160 }
161
162 mod display {
163 use pretty_assertions::assert_eq;
164
165 use super::*;
166
167 #[test]
168 fn it_formats_with_tab_prefix() {
169 let note = Note::from_lines(vec!["line one", "line two"]);
170
171 assert_eq!(note.to_string(), "\t\tline one\n\t\tline two");
172 }
173 }
174
175 mod from_str {
176 use pretty_assertions::assert_eq;
177
178 use super::*;
179
180 #[test]
181 fn it_splits_on_newlines() {
182 let note = Note::from_str("line one\nline two\nline three");
183
184 assert_eq!(note.lines(), &["line one", "line two", "line three"]);
185 }
186 }
187
188 mod is_empty {
189 use super::*;
190
191 #[test]
192 fn it_returns_true_for_empty_note() {
193 let note = Note::new();
194
195 assert!(note.is_empty());
196 }
197
198 #[test]
199 fn it_returns_true_for_blank_lines_only() {
200 let note = Note::from_lines(vec!["", " ", "\t"]);
201
202 assert!(note.is_empty());
203 }
204
205 #[test]
206 fn it_returns_false_for_content() {
207 let note = Note::from_lines(vec!["hello"]);
208
209 assert!(!note.is_empty());
210 }
211 }
212
213 mod to_line {
214 use pretty_assertions::assert_eq;
215
216 use super::*;
217
218 #[test]
219 fn it_joins_with_separator() {
220 let note = Note::from_lines(vec!["one", "two", "three"]);
221
222 assert_eq!(note.to_line(" "), "one two three");
223 }
224
225 #[test]
226 fn it_compresses_before_joining() {
227 let note = Note::from_lines(vec!["", "one", "", "", "two", ""]);
228
229 assert_eq!(note.to_line("|"), "one||two");
230 }
231 }
232}