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 self.lines = self.compressed_lines().map(String::from).collect();
46 }
47
48 pub fn is_empty(&self) -> bool {
50 self.lines.is_empty() || self.lines.iter().all(|l| l.trim().is_empty())
51 }
52
53 pub fn len(&self) -> usize {
55 self.lines.len()
56 }
57
58 pub fn lines(&self) -> &[String] {
60 &self.lines
61 }
62
63 pub fn to_line(&self, separator: &str) -> String {
65 let lines: Vec<&str> = self.compressed_lines().collect();
66 lines.join(separator)
67 }
68
69 fn compressed_lines(&self) -> impl Iterator<Item = &str> {
71 let mut prev_blank = true; let mut lines: Vec<&str> = Vec::new();
73 for line in &self.lines {
74 let trimmed = line.trim_end();
75 let is_blank = trimmed.trim().is_empty();
76 if is_blank {
77 if !prev_blank {
78 lines.push("");
79 }
80 prev_blank = true;
81 } else {
82 lines.push(trimmed);
83 prev_blank = false;
84 }
85 }
86 while lines.last().is_some_and(|l| l.trim().is_empty()) {
88 lines.pop();
89 }
90 lines.into_iter()
91 }
92}
93
94impl Display for Note {
95 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
97 for (i, line) in self.compressed_lines().enumerate() {
98 if i > 0 {
99 writeln!(f)?;
100 }
101 write!(f, "\t\t{line}")?;
102 }
103 Ok(())
104 }
105}
106
107#[cfg(test)]
108mod test {
109 use super::*;
110
111 mod compress {
112 use pretty_assertions::assert_eq;
113
114 use super::*;
115
116 #[test]
117 fn it_collapses_consecutive_blank_lines() {
118 let mut note = Note::from_lines(vec!["first", "", "", "", "second"]);
119
120 note.compress();
121
122 assert_eq!(note.lines(), &["first", "", "second"]);
123 }
124
125 #[test]
126 fn it_removes_leading_blank_lines() {
127 let mut note = Note::from_lines(vec!["", "", "content"]);
128
129 note.compress();
130
131 assert_eq!(note.lines(), &["content"]);
132 }
133
134 #[test]
135 fn it_removes_trailing_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_trims_trailing_whitespace_from_lines() {
145 let mut note = Note::from_lines(vec!["hello ", "world "]);
146
147 note.compress();
148
149 assert_eq!(note.lines(), &["hello", "world"]);
150 }
151 }
152
153 mod display {
154 use pretty_assertions::assert_eq;
155
156 use super::*;
157
158 #[test]
159 fn it_formats_with_tab_prefix() {
160 let note = Note::from_lines(vec!["line one", "line two"]);
161
162 assert_eq!(note.to_string(), "\t\tline one\n\t\tline two");
163 }
164 }
165
166 mod from_str {
167 use pretty_assertions::assert_eq;
168
169 use super::*;
170
171 #[test]
172 fn it_splits_on_newlines() {
173 let note = Note::from_str("line one\nline two\nline three");
174
175 assert_eq!(note.lines(), &["line one", "line two", "line three"]);
176 }
177 }
178
179 mod is_empty {
180 use super::*;
181
182 #[test]
183 fn it_returns_true_for_empty_note() {
184 let note = Note::new();
185
186 assert!(note.is_empty());
187 }
188
189 #[test]
190 fn it_returns_true_for_blank_lines_only() {
191 let note = Note::from_lines(vec!["", " ", "\t"]);
192
193 assert!(note.is_empty());
194 }
195
196 #[test]
197 fn it_returns_false_for_content() {
198 let note = Note::from_lines(vec!["hello"]);
199
200 assert!(!note.is_empty());
201 }
202 }
203
204 mod to_line {
205 use pretty_assertions::assert_eq;
206
207 use super::*;
208
209 #[test]
210 fn it_joins_with_separator() {
211 let note = Note::from_lines(vec!["one", "two", "three"]);
212
213 assert_eq!(note.to_line(" "), "one two three");
214 }
215
216 #[test]
217 fn it_compresses_before_joining() {
218 let note = Note::from_lines(vec!["", "one", "", "", "two", ""]);
219
220 assert_eq!(note.to_line("|"), "one||two");
221 }
222 }
223}