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