Skip to content

Commit ae5f8bf

Browse files
authored
Merge pull request #96 from andymac-2/fix-art-widths
Fixed ascii art widths.
2 parents 962dc42 + 843b122 commit ae5f8bf

File tree

2 files changed

+310
-86
lines changed

2 files changed

+310
-86
lines changed

Diff for: src/ascii_art.rs

+293
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
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

Comments
 (0)