1use owo_colors::{AnsiColors, DynColors, OwoColorize, Style};
47use std::fmt::Write;
48
49pub struct AsciiArt<'a> {
51 content: Box<dyn 'a + Iterator<Item = &'a str>>,
52 colors: &'a [DynColors],
53 bold: bool,
54 start: usize,
55 end: usize,
56}
57impl<'a> AsciiArt<'a> {
58 pub fn new(input: &'a str, colors: &'a [DynColors], bold: bool) -> AsciiArt<'a> {
59 let mut lines: Vec<_> = input.lines().skip_while(|line| line.is_empty()).collect();
60 while let Some(line) = lines.last() {
61 if Tokens(line).is_empty() {
62 lines.pop();
63 } else {
64 break;
65 }
66 }
67
68 let (start, end) = get_min_start_max_end(&lines);
69
70 AsciiArt {
71 content: Box::new(lines.into_iter()),
72 colors,
73 bold,
74 start,
75 end,
76 }
77 }
78
79 pub fn width(&self) -> usize {
80 assert!(self.end >= self.start);
81 self.end - self.start
82 }
83}
84
85fn get_min_start_max_end(lines: &[&str]) -> (usize, usize) {
86 lines
87 .iter()
88 .map(|line| {
89 let line_start = Tokens(line).leading_spaces();
90 let line_end = Tokens(line).true_length();
91 (line_start, line_end)
92 })
93 .fold((usize::MAX, 0), |(acc_s, acc_e), (line_s, line_e)| {
94 (acc_s.min(line_s), acc_e.max(line_e))
95 })
96}
97
98impl Iterator for AsciiArt<'_> {
101 type Item = String;
102 fn next(&mut self) -> Option<String> {
103 self.content
104 .next()
105 .map(|line| Tokens(line).render(self.colors, self.start, self.end, self.bold))
106 }
107}
108
109#[derive(Clone, Debug, PartialEq, Eq)]
110enum Token {
111 Color(u32),
112 Char(char),
113 Space,
114}
115impl std::fmt::Display for Token {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 match *self {
118 Token::Color(c) => write!(f, "{{{c}}}"),
119 Token::Char(c) => write!(f, "{c}"),
120 Token::Space => write!(f, " "),
121 }
122 }
123}
124impl Token {
125 fn is_solid(&self) -> bool {
126 matches!(*self, Token::Char(_))
127 }
128 fn is_space(&self) -> bool {
129 matches!(*self, Token::Space)
130 }
131 fn has_zero_width(&self) -> bool {
132 matches!(*self, Token::Color(_))
133 }
134}
135
136#[derive(Clone, Debug)]
138struct Tokens<'a>(&'a str);
139impl Iterator for Tokens<'_> {
140 type Item = Token;
141 fn next(&mut self) -> Option<Token> {
142 let (s, tok) = color_token(self.0)
143 .or_else(|| space_token(self.0))
144 .or_else(|| char_token(self.0))?;
145
146 self.0 = s;
147 Some(tok)
148 }
149}
150
151impl<'a> Tokens<'a> {
152 fn is_empty(&mut self) -> bool {
153 for token in self {
154 if token.is_solid() {
155 return false;
156 }
157 }
158 true
159 }
160 fn true_length(&mut self) -> usize {
161 let mut last_non_space = 0;
162 let mut last = 0;
163 for token in self {
164 if token.has_zero_width() {
165 continue;
166 }
167 last += 1;
168 if !token.is_space() {
169 last_non_space = last;
170 }
171 }
172 last_non_space
173 }
174 fn leading_spaces(&mut self) -> usize {
175 self.take_while(|token| !token.is_solid())
176 .filter(Token::is_space)
177 .count()
178 }
179 fn truncate(self, mut start: usize, end: usize) -> impl 'a + Iterator<Item = Token> {
180 assert!(start <= end);
181 let mut width = end - start;
182
183 self.filter(move |token| {
184 if start > 0 && !token.has_zero_width() {
185 start -= 1;
186 return false;
187 }
188 true
189 })
190 .take_while(move |token| {
191 if width == 0 {
192 return false;
193 }
194 if !token.has_zero_width() {
195 width -= 1;
196 }
197 true
198 })
199 }
200 fn render(self, colors: &[DynColors], start: usize, end: usize, bold: bool) -> String {
202 assert!(start <= end);
203 let mut width = end - start;
204 let mut colored_segment = String::new();
205 let mut whole_string = String::new();
206 let mut color = &DynColors::Ansi(AnsiColors::Default);
207
208 self.truncate(start, end).for_each(|token| match token {
209 Token::Char(chr) => {
210 width = width.saturating_sub(1);
211 colored_segment.push(chr);
212 }
213 Token::Color(col) => {
214 add_styled_segment(&mut whole_string, &colored_segment, *color, bold);
215 colored_segment = String::new();
216 color = colors
217 .get(col as usize)
218 .unwrap_or(&DynColors::Ansi(AnsiColors::Default));
219 }
220 Token::Space => {
221 width = width.saturating_sub(1);
222 colored_segment.push(' ');
223 }
224 });
225
226 add_styled_segment(&mut whole_string, &colored_segment, *color, bold);
227 (0..width).for_each(|_| whole_string.push(' '));
228 whole_string
229 }
230}
231
232fn succeed_when<I>(predicate: impl FnOnce(I) -> bool) -> impl FnOnce(I) -> Option<()> {
235 |input| {
236 if predicate(input) {
237 Some(())
238 } else {
239 None
240 }
241 }
242}
243
244fn add_styled_segment(base: &mut String, segment: &str, color: DynColors, bold: bool) {
245 let mut style = Style::new().color(color);
246 if bold {
247 style = style.bold();
248 }
249 let formatted_segment = segment.style(style);
250 let _ = write!(base, "{formatted_segment}");
251}
252
253type ParseResult<'a, R> = Option<(&'a str, R)>;
256
257fn token<R>(s: &str, predicate: impl FnOnce(char) -> Option<R>) -> ParseResult<R> {
258 let mut chars = s.chars();
259 let token = chars.next()?;
260 let result = predicate(token)?;
261 Some((chars.as_str(), result))
262}
263
264fn color_token(s: &str) -> ParseResult<Token> {
268 let (s, ()) = token(s, succeed_when(|c| c == '{'))?;
269 let (s, color_index) = token(s, |c| c.to_digit(10))?;
270 let (s, ()) = token(s, succeed_when(|c| c == '}'))?;
271 Some((s, Token::Color(color_index)))
272}
273
274fn space_token(s: &str) -> ParseResult<Token> {
276 token(s, succeed_when(|c| c == ' ')).map(|(s, ())| (s, Token::Space))
277}
278
279fn char_token(s: &str) -> ParseResult<Token> {
281 token(s, |c| Some(Token::Char(c)))
282}
283
284#[cfg(test)]
285mod test {
286 use super::*;
287
288 #[test]
289 fn test_get_min_start_max_end() {
290 let lines = [
291 " xxx",
292 " xxx",
293 " oo",
294 " o",
295 " xx",
296 ];
297 assert_eq!(get_min_start_max_end(&lines), (3, 29));
298 }
299
300 #[test]
301 fn space_parses() {
302 assert_eq!(space_token(" "), Some(("", Token::Space)));
303 assert_eq!(space_token(" hello"), Some(("hello", Token::Space)));
304 assert_eq!(space_token(" "), Some((" ", Token::Space)));
305 assert_eq!(space_token(" {1}{2}"), Some(("{1}{2}", Token::Space)));
306 }
307
308 #[test]
309 fn color_indicator_parses() {
310 assert_eq!(color_token("{1}"), Some(("", Token::Color(1))));
311 assert_eq!(color_token("{9} "), Some((" ", Token::Color(9))));
312 }
313
314 #[test]
315 fn leading_spaces_counts_correctly() {
316 assert_eq!(Tokens("").leading_spaces(), 0);
317 assert_eq!(Tokens(" ").leading_spaces(), 5);
318 assert_eq!(Tokens(" a;lksjf;a").leading_spaces(), 5);
319 assert_eq!(Tokens(" {1} {5} {9} a").leading_spaces(), 6);
320 }
321
322 #[test]
323 fn render() {
324 let colors_shim = Vec::new();
325
326 assert_eq!(
327 Tokens("").render(&colors_shim, 0, 0, true),
328 "\u{1b}[39;1m\u{1b}[0m"
329 );
330
331 assert_eq!(
332 Tokens(" ").render(&colors_shim, 0, 0, true),
333 "\u{1b}[39;1m\u{1b}[0m"
334 );
335
336 assert_eq!(
337 Tokens(" ").render(&colors_shim, 0, 5, true),
338 "\u{1b}[39;1m \u{1b}[0m"
339 );
340
341 assert_eq!(
342 Tokens(" ").render(&colors_shim, 1, 5, true),
343 "\u{1b}[39;1m \u{1b}[0m"
344 );
345
346 assert_eq!(
347 Tokens(" ").render(&colors_shim, 3, 5, true),
348 "\u{1b}[39;1m \u{1b}[0m"
349 );
350
351 assert_eq!(
352 Tokens(" ").render(&colors_shim, 0, 4, true),
353 "\u{1b}[39;1m \u{1b}[0m"
354 );
355
356 assert_eq!(
357 Tokens(" ").render(&colors_shim, 0, 3, true),
358 "\u{1b}[39;1m \u{1b}[0m"
359 );
360
361 assert_eq!(
363 Tokens("███").render(Vec::new().as_slice(), 0, 3, true),
364 "\u{1b}[39;1m███\u{1b}[0m"
365 );
366
367 assert_eq!(
368 Tokens(" {1} {5} {9} a").render(&colors_shim, 4, 10, true),
369 "\u{1b}[39;1m\u{1b}[0m\u{1b}[39;1m\u{1b}[0m\u{1b}[39;1m \u{1b}[0m\u{1b}[39;1m a\u{1b}[0m "
370 );
371
372 assert_eq!(
374 Tokens(" ").render(&colors_shim, 0, 0, false),
375 "\u{1b}[39m\u{1b}[0m"
376 );
377 assert_eq!(
378 Tokens(" ").render(&colors_shim, 0, 5, false),
379 "\u{1b}[39m \u{1b}[0m"
380 );
381 }
382
383 #[test]
384 fn truncate() {
385 assert_eq!(
386 Tokens("").truncate(0, 0).collect::<Vec<_>>(),
387 Tokens("").collect::<Vec<_>>()
388 );
389
390 assert_eq!(
391 Tokens(" ").truncate(0, 0).collect::<Vec<_>>(),
392 Tokens("").collect::<Vec<_>>()
393 );
394 assert_eq!(
395 Tokens(" ").truncate(0, 5).collect::<Vec<_>>(),
396 Tokens(" ").collect::<Vec<_>>()
397 );
398 assert_eq!(
399 Tokens(" ").truncate(1, 5).collect::<Vec<_>>(),
400 Tokens(" ").collect::<Vec<_>>()
401 );
402 assert_eq!(
403 Tokens(" ").truncate(3, 5).collect::<Vec<_>>(),
404 Tokens(" ").collect::<Vec<_>>()
405 );
406 assert_eq!(
407 Tokens(" ").truncate(0, 4).collect::<Vec<_>>(),
408 Tokens(" ").collect::<Vec<_>>()
409 );
410 assert_eq!(
411 Tokens(" ").truncate(0, 3).collect::<Vec<_>>(),
412 Tokens(" ").collect::<Vec<_>>()
413 );
414
415 assert_eq!(
416 Tokens(" {1} {5} {9} a")
417 .truncate(4, 10)
418 .collect::<Vec<_>>(),
419 Tokens("{1}{5} {9} a").collect::<Vec<_>>()
420 );
421 }
422}