|
| 1 | +use colored::{Color, Colorize}; |
| 2 | + |
| 3 | +pub struct AsciiArt<'a> { |
| 4 | + content: Box<dyn 'a + Iterator<Item = &'a str>>, |
| 5 | + colors: Vec<Color>, |
| 6 | + start: usize, |
| 7 | + end: usize, |
| 8 | +} |
| 9 | +impl<'a> AsciiArt<'a> { |
| 10 | + pub fn new(input: &'a str, colors: Vec<Color>) -> AsciiArt<'a> { |
| 11 | + let mut lines: Vec<_> = input.lines().skip_while(|line| line.is_empty()).collect(); |
| 12 | + while let Some(line) = lines.last() { |
| 13 | + if Tokens(line).is_empty() { |
| 14 | + lines.pop(); |
| 15 | + } |
| 16 | + break; |
| 17 | + } |
| 18 | + |
| 19 | + let (start, end) = lines |
| 20 | + .iter() |
| 21 | + .map(|line| { |
| 22 | + let line_start = Tokens(line).leading_spaces(); |
| 23 | + let line_end = Tokens(line).true_length(); |
| 24 | + (line_start, line_end) |
| 25 | + }) |
| 26 | + .fold((std::usize::MAX, 0), |(acc_s, acc_e), (line_s, line_e)| { |
| 27 | + (acc_s.min(line_s), acc_e.max(line_e)) |
| 28 | + }); |
| 29 | + |
| 30 | + AsciiArt { |
| 31 | + content: Box::new(lines.into_iter()), |
| 32 | + colors: colors, |
| 33 | + start: start, |
| 34 | + end: end, |
| 35 | + } |
| 36 | + } |
| 37 | + pub fn width(&self) -> usize { |
| 38 | + assert!(self.end >= self.start); |
| 39 | + self.end - self.start |
| 40 | + } |
| 41 | +} |
| 42 | + |
| 43 | +/// Produces a series of lines which have been automatically truncated to the |
| 44 | +/// correct width |
| 45 | +impl<'a> Iterator for AsciiArt<'a> { |
| 46 | + type Item = String; |
| 47 | + fn next(&mut self) -> Option<String> { |
| 48 | + self.content |
| 49 | + .next() |
| 50 | + .map(|line| Tokens(line).render(&self.colors, self.start, self.end)) |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +#[derive(Clone, Debug, PartialEq, Eq)] |
| 55 | +enum Token { |
| 56 | + Color(u32), |
| 57 | + Char(char), |
| 58 | + Space, |
| 59 | +} |
| 60 | +impl std::fmt::Display for Token { |
| 61 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 62 | + match *self { |
| 63 | + Token::Color(c) => write!(f, "{{{}}}", c), |
| 64 | + Token::Char(c) => write!(f, "{}", c), |
| 65 | + Token::Space => write!(f, " "), |
| 66 | + } |
| 67 | + } |
| 68 | +} |
| 69 | +impl Token { |
| 70 | + fn is_solid(&self) -> bool { |
| 71 | + match *self { |
| 72 | + Token::Char(_) => true, |
| 73 | + _ => false, |
| 74 | + } |
| 75 | + } |
| 76 | + fn is_space(&self) -> bool { |
| 77 | + match *self { |
| 78 | + Token::Space => true, |
| 79 | + _ => false, |
| 80 | + } |
| 81 | + } |
| 82 | + fn has_zero_width(&self) -> bool { |
| 83 | + match *self { |
| 84 | + Token::Color(_) => true, |
| 85 | + _ => false, |
| 86 | + } |
| 87 | + } |
| 88 | +} |
| 89 | + |
| 90 | +/// An iterator over tokens found within the *.ascii format. |
| 91 | +#[derive(Clone, Debug)] |
| 92 | +struct Tokens<'a>(&'a str); |
| 93 | +impl<'a> Iterator for Tokens<'a> { |
| 94 | + type Item = Token; |
| 95 | + fn next(&mut self) -> Option<Token> { |
| 96 | + let (s, tok) = color_token(self.0) |
| 97 | + .or_else(|| space_token(self.0)) |
| 98 | + .or_else(|| char_token(self.0))?; |
| 99 | + |
| 100 | + self.0 = s; |
| 101 | + Some(tok) |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +impl<'a> Tokens<'a> { |
| 106 | + fn is_empty(&mut self) -> bool { |
| 107 | + for token in self { |
| 108 | + if token.is_solid() { |
| 109 | + return false; |
| 110 | + } |
| 111 | + } |
| 112 | + true |
| 113 | + } |
| 114 | + fn true_length(&mut self) -> usize { |
| 115 | + let mut last_non_space = 0; |
| 116 | + let mut last = 0; |
| 117 | + for token in self { |
| 118 | + if token.has_zero_width() { |
| 119 | + continue; |
| 120 | + } |
| 121 | + last += 1; |
| 122 | + if !token.is_space() { |
| 123 | + last_non_space = last; |
| 124 | + } |
| 125 | + } |
| 126 | + last_non_space |
| 127 | + } |
| 128 | + fn leading_spaces(&mut self) -> usize { |
| 129 | + self.take_while(|token| !token.is_solid()) |
| 130 | + .filter(|token| token.is_space()) |
| 131 | + .count() |
| 132 | + } |
| 133 | + fn truncate(self, mut start: usize, end: usize) -> impl 'a + Iterator<Item = Token> { |
| 134 | + assert!(start <= end); |
| 135 | + let mut width = end - start; |
| 136 | + |
| 137 | + self.filter(move |token| { |
| 138 | + if start > 0 && !token.has_zero_width() { |
| 139 | + start -= 1; |
| 140 | + return false; |
| 141 | + } |
| 142 | + true |
| 143 | + }) |
| 144 | + .take_while(move |token| { |
| 145 | + if width == 0 { |
| 146 | + return false; |
| 147 | + } |
| 148 | + if !token.has_zero_width() { |
| 149 | + width -= 1; |
| 150 | + } |
| 151 | + true |
| 152 | + }) |
| 153 | + } |
| 154 | + /// render a truncated line of tokens. |
| 155 | + fn render(self, colors: &Vec<Color>, start: usize, end: usize) -> String { |
| 156 | + assert!(start <= end); |
| 157 | + let mut width = end - start; |
| 158 | + let mut colored_segment = String::new(); |
| 159 | + let mut whole_string = String::new(); |
| 160 | + let mut color = &Color::White; |
| 161 | + |
| 162 | + self.truncate(start, end).for_each(|token| { |
| 163 | + match token { |
| 164 | + Token::Char(chr) => { |
| 165 | + width = width.saturating_sub(1); |
| 166 | + colored_segment.push(chr); |
| 167 | + } |
| 168 | + Token::Color(col) => { |
| 169 | + add_colored_segment(&mut whole_string, &colored_segment, color); |
| 170 | + colored_segment = String::new(); |
| 171 | + color = colors.get(col as usize).unwrap_or(&Color::White); |
| 172 | + } |
| 173 | + Token::Space => { |
| 174 | + width = width.saturating_sub(1); |
| 175 | + colored_segment.push(' ') |
| 176 | + } |
| 177 | + }; |
| 178 | + }); |
| 179 | + |
| 180 | + add_colored_segment(&mut whole_string, &colored_segment, color); |
| 181 | + (0..width).for_each(|_| whole_string.push(' ')); |
| 182 | + whole_string |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +// Utility functions |
| 187 | + |
| 188 | +fn succeed_when<I>(predicate: impl FnOnce(I) -> bool) -> impl FnOnce(I) -> Option<()> { |
| 189 | + |input| { |
| 190 | + if predicate(input) { |
| 191 | + Some(()) |
| 192 | + } else { |
| 193 | + None |
| 194 | + } |
| 195 | + } |
| 196 | +} |
| 197 | + |
| 198 | +fn add_colored_segment(base: &mut String, segment: &String, color: &Color) { |
| 199 | + base.push_str(&format!("{}", segment.color(*color).bold())) |
| 200 | +} |
| 201 | + |
| 202 | +// Basic combinators |
| 203 | + |
| 204 | +type ParseResult<'a, R> = Option<(&'a str, R)>; |
| 205 | + |
| 206 | +fn token<'a, R>(s: &'a str, predicate: impl FnOnce(char) -> Option<R>) -> ParseResult<'a, R> { |
| 207 | + let token = s.chars().next()?; |
| 208 | + let result = predicate(token)?; |
| 209 | + Some((s.get(1..).unwrap(), result)) |
| 210 | +} |
| 211 | + |
| 212 | +// Parsers |
| 213 | + |
| 214 | +/// Parses a color indiator of the format `{n}` where `n` is a digit. |
| 215 | +fn color_token(s: &str) -> ParseResult<Token> { |
| 216 | + let (s, _) = token(s, succeed_when(|c| c == '{'))?; |
| 217 | + let (s, color_index) = token(s, |c| c.to_digit(10))?; |
| 218 | + let (s, _) = token(s, succeed_when(|c| c == '}'))?; |
| 219 | + Some((s, Token::Color(color_index))) |
| 220 | +} |
| 221 | + |
| 222 | +/// Parses a space. |
| 223 | +fn space_token(s: &str) -> ParseResult<Token> { |
| 224 | + token(s, succeed_when(|c| c == ' ')).map(|(s, _)| (s, Token::Space)) |
| 225 | +} |
| 226 | + |
| 227 | +/// Parses any arbitrary character. This cannot fail. |
| 228 | +fn char_token(s: &str) -> ParseResult<Token> { |
| 229 | + token(s, |c| Some(Token::Char(c))) |
| 230 | +} |
| 231 | + |
| 232 | +#[cfg(test)] |
| 233 | +mod test { |
| 234 | + use super::*; |
| 235 | + |
| 236 | + #[test] |
| 237 | + fn space_parses() { |
| 238 | + assert_eq!(space_token(" "), Some(("", Token::Space))); |
| 239 | + assert_eq!(space_token(" hello"), Some(("hello", Token::Space))); |
| 240 | + assert_eq!(space_token(" "), Some((" ", Token::Space))); |
| 241 | + assert_eq!(space_token(" {1}{2}"), Some(("{1}{2}", Token::Space))); |
| 242 | + } |
| 243 | + |
| 244 | + #[test] |
| 245 | + fn color_indicator_parses() { |
| 246 | + assert_eq!(color_token("{1}"), Some(("", Token::Color(1)))); |
| 247 | + assert_eq!(color_token("{9} "), Some((" ", Token::Color(9)))); |
| 248 | + } |
| 249 | + |
| 250 | + #[test] |
| 251 | + fn leading_spaces_counts_correctly() { |
| 252 | + assert_eq!(Tokens("").leading_spaces(), 0); |
| 253 | + assert_eq!(Tokens(" ").leading_spaces(), 5); |
| 254 | + assert_eq!(Tokens(" a;lksjf;a").leading_spaces(), 5); |
| 255 | + assert_eq!(Tokens(" {1} {5} {9} a").leading_spaces(), 6); |
| 256 | + } |
| 257 | + |
| 258 | + #[test] |
| 259 | + fn truncate() { |
| 260 | + let colors_shim = Vec::new(); |
| 261 | + assert_eq!(Tokens("").render(&colors_shim, 0, 0), "\u{1b}[37m\u{1b}[0m"); |
| 262 | + |
| 263 | + assert_eq!( |
| 264 | + Tokens(" ").render(&colors_shim, 0, 0), |
| 265 | + "\u{1b}[37m\u{1b}[0m" |
| 266 | + ); |
| 267 | + assert_eq!( |
| 268 | + Tokens(" ").render(&colors_shim, 0, 5), |
| 269 | + "\u{1b}[37m \u{1b}[0m" |
| 270 | + ); |
| 271 | + assert_eq!( |
| 272 | + Tokens(" ").render(&colors_shim, 1, 5), |
| 273 | + "\u{1b}[37m \u{1b}[0m" |
| 274 | + ); |
| 275 | + assert_eq!( |
| 276 | + Tokens(" ").render(&colors_shim, 3, 5), |
| 277 | + "\u{1b}[37m \u{1b}[0m" |
| 278 | + ); |
| 279 | + assert_eq!( |
| 280 | + Tokens(" ").render(&colors_shim, 0, 4), |
| 281 | + "\u{1b}[37m \u{1b}[0m" |
| 282 | + ); |
| 283 | + assert_eq!( |
| 284 | + Tokens(" ").render(&colors_shim, 0, 3), |
| 285 | + "\u{1b}[37m \u{1b}[0m" |
| 286 | + ); |
| 287 | + |
| 288 | + assert_eq!( |
| 289 | + Tokens(" {1} {5} {9} a").render(&colors_shim, 4, 10), |
| 290 | + "\u{1b}[37m\u{1b}[0m\u{1b}[37m\u{1b}[0m\u{1b}[37m \u{1b}[0m\u{1b}[37m a\u{1b}[0m " |
| 291 | + ); |
| 292 | + } |
| 293 | +} |
0 commit comments