use crate::component::Component; // (line (centered (limited (padded "hello world")))) #[derive(Debug, Clone)] pub enum Token { Centered(Box), Limited(Box, usize), CharPad(Box, char, usize), String(String), } impl From for Token { fn from(value: String) -> Self { Token::String(value) } } impl Token { pub fn centered(self) -> Self { Self::Centered(Box::new(self)) } pub fn limited(self, lim: usize) -> Self { Self::Limited(Box::new(self), lim) } pub fn padded(self, pad_to: usize) -> Self { Self::CharPad(Box::new(self), ' ', pad_to) } pub fn str_len(&self) -> usize { self.walk_to_end().len() } pub fn pad_char(self, c: char, pad_to: usize) -> Self { Self::CharPad(Box::new(self), c, pad_to) } fn walk_to_end(&self) -> &String { match self { Token::String(s) => s, Token::Centered(t) => t.walk_to_end(), Token::Limited(t, _) => t.walk_to_end(), Token::CharPad(t, _, _) => t.walk_to_end(), } } fn with_width(self, width: usize) -> Vec { match self { Token::String(s) => vec![Component::String(s)], Token::Centered(cnt) => cnt .with_width(width) .into_iter() .map(|comp| { if let Component::String(s) = comp { let str_len = s.len(); let x = if str_len > width { 0 } else { (width - str_len) / 2 }; vec![Component::X(x), Component::String(s)] } else { vec![comp] } }) .flatten() .collect(), Token::Limited(s, lim) => s .with_width(width) .into_iter() .map(|cmp| { if let Component::String(mut s) = cmp { s.truncate(lim); Component::String(s) } else { cmp } }) .collect(), Token::CharPad(s, c, pad_to) => s .with_width(width) .into_iter() .map(|comp| { if let Component::String(s) = comp { if s.len() >= pad_to { return Component::String(s); } Component::String(format!( "{}{}", s, c.to_string().repeat(pad_to - s.len()) )) } else { comp } }) .collect(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_token_gen() { const WIDTH: usize = 20; vec![ ( "string -> string", Token::String("hello".into()), vec![Component::String("hello".into())], ), ( "string -> limited", Token::String( "This string is too long for this!".into(), ) .limited(4), vec![Component::String("This".into())], ), ( "string -> limit -> pad_to", Token::String( "This string is too long, but some".into(), ) .limited(10) .padded(15), vec![Component::String("This strin ".into())], ), ( "center limited string", Token::String("Ahhh this won't go far".into()) .limited(10) .centered(), vec![ Component::X((WIDTH - 10) / 2), Component::String("Ahhh this ".into()), ], ), ( "padded string with underscores is centered", Token::String("It was...".into()) .pad_char('_', 15) .centered(), vec![ Component::X(WIDTH - 3), Component::String("It was...______".into()), ], ), ] .into_iter() .for_each(|test| { let (name, token, expected) = test; eprintln!("token: {:#?}", &token); let actual = token.with_width(WIDTH); let actual_fmt = format!("{:#?}", &actual); assert!( expected .iter() .zip(actual.iter()) .filter(|&(l, r)| l != r) .count() == 0, "{} expected: {:#?} actual: {}", name, expected, actual_fmt, ) }) } }