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