use std::{
fmt::{Display, Formatter, Result as FmtResult},
str::FromStr,
};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Note {
lines: Vec<String>,
}
impl Note {
pub fn from_lines(lines: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
lines: lines.into_iter().map(Into::into).collect(),
}
}
pub fn from_text(text: &str) -> Self {
Self {
lines: text.lines().map(String::from).collect(),
}
}
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, text: impl Into<String>) {
let text = text.into();
for line in text.lines() {
self.lines.push(line.to_string());
}
}
pub fn compress(&mut self) {
self.lines = self.compressed_lines().map(String::from).collect();
}
pub fn is_empty(&self) -> bool {
self.lines.is_empty() || self.lines.iter().all(|l| l.trim().is_empty())
}
pub fn len(&self) -> usize {
self.lines.len()
}
pub fn lines(&self) -> &[String] {
&self.lines
}
pub fn to_line(&self, separator: &str) -> String {
let lines: Vec<&str> = self.compressed_lines().collect();
lines.join(separator)
}
fn compressed_lines(&self) -> impl Iterator<Item = &str> {
let mut prev_blank = true; let mut lines: Vec<&str> = Vec::new();
for line in &self.lines {
let trimmed = line.trim_end();
let is_blank = trimmed.trim().is_empty();
if is_blank {
if !prev_blank {
lines.push("");
}
prev_blank = true;
} else {
lines.push(trimmed);
prev_blank = false;
}
}
while lines.last().is_some_and(|l| l.trim().is_empty()) {
lines.pop();
}
lines.into_iter()
}
}
impl Display for Note {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
for (i, line) in self.compressed_lines().enumerate() {
if i > 0 {
writeln!(f)?;
}
write!(f, "\t\t{line}")?;
}
Ok(())
}
}
impl FromStr for Note {
type Err = std::convert::Infallible;
fn from_str(text: &str) -> std::result::Result<Self, Self::Err> {
Ok(Self {
lines: text.lines().map(String::from).collect(),
})
}
}
#[cfg(test)]
mod test {
use super::*;
mod compress {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_collapses_consecutive_blank_lines() {
let mut note = Note::from_lines(vec!["first", "", "", "", "second"]);
note.compress();
assert_eq!(note.lines(), &["first", "", "second"]);
}
#[test]
fn it_removes_leading_blank_lines() {
let mut note = Note::from_lines(vec!["", "", "content"]);
note.compress();
assert_eq!(note.lines(), &["content"]);
}
#[test]
fn it_removes_trailing_blank_lines() {
let mut note = Note::from_lines(vec!["content", "", ""]);
note.compress();
assert_eq!(note.lines(), &["content"]);
}
#[test]
fn it_trims_trailing_whitespace_from_lines() {
let mut note = Note::from_lines(vec!["hello ", "world "]);
note.compress();
assert_eq!(note.lines(), &["hello", "world"]);
}
}
mod display {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_formats_with_tab_prefix() {
let note = Note::from_lines(vec!["line one", "line two"]);
assert_eq!(note.to_string(), "\t\tline one\n\t\tline two");
}
}
mod from_text {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_splits_on_newlines() {
let note = Note::from_text("line one\nline two\nline three");
assert_eq!(note.lines(), &["line one", "line two", "line three"]);
}
}
mod is_empty {
use super::*;
#[test]
fn it_returns_true_for_empty_note() {
let note = Note::new();
assert!(note.is_empty());
}
#[test]
fn it_returns_true_for_blank_lines_only() {
let note = Note::from_lines(vec!["", " ", "\t"]);
assert!(note.is_empty());
}
#[test]
fn it_returns_false_for_content() {
let note = Note::from_lines(vec!["hello"]);
assert!(!note.is_empty());
}
}
mod to_line {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_joins_with_separator() {
let note = Note::from_lines(vec!["one", "two", "three"]);
assert_eq!(note.to_line(" "), "one two three");
}
#[test]
fn it_compresses_before_joining() {
let note = Note::from_lines(vec!["", "one", "", "", "two", ""]);
assert_eq!(note.to_line("|"), "one||two");
}
}
}