artify 1.0.0

Display colorized ascii art to the terminal
Documentation
//! # artify
//!
//! Provides the ascii template interface.
//!
//! ```rust,no_run
//! use artify::AsciiArt;
//! use owo_colors::DynColors;
//! use owo_colors::AnsiColors;
//!
//! const ASCII: &str = r#"
//! {2}            .:--::////::--.`
//! {1}        `/yNMMNho{2}////////////:.
//! {1}      `+NMMMMMMMMmy{2}/////////////:`
//! {0}    `-:::{1}ohNMMMMMMMNy{2}/////////////:`
//! {0}   .::::::::{1}odMMMMMMMNy{2}/////////////-
//! {0}  -:::::::::::{1}/hMMMMMMMmo{2}////////////-
//! {0} .::::::::::::::{1}oMMMMMMMMh{2}////////////-
//! {0}`:::::::::::::{1}/dMMMMMMMMMMNo{2}///////////`
//! {0}-::::::::::::{1}sMMMMMMmMMMMMMMy{2}//////////-
//! {0}-::::::::::{1}/dMMMMMMs{0}:{1}+NMMMMMMd{2}/////////:
//! {0}-:::::::::{1}+NMMMMMm/{0}:::{1}/dMMMMMMm+{2}///////:
//! {0}-::::::::{1}sMMMMMMh{0}:::::::{1}dMMMMMMm+{2}//////-
//! {0}`:::::::{1}sMMMMMMy{0}:::::::::{1}dMMMMMMm+{2}/////`
//! {0} .:::::{1}sMMMMMMs{0}:::::::::::{1}mMMMMMMd{2}////-
//! {0}  -:::{1}sMMMMMMy{0}::::::::::::{1}/NMMMMMMh{2}//-
//! {0}   .:{1}+MMMMMMd{0}::::::::::::::{1}oMMMMMMMo{2}-
//! {1}    `yMMMMMN/{0}:::::::::::::::{1}hMMMMMh.
//! {1}      -yMMMo{0}::::::::::::::::{1}/MMMy-
//! {1}        `/s{0}::::::::::::::::::{1}o/`
//! {0}            ``.---::::---..`
//! "#;
//!
//! let colors: Vec<DynColors> = vec![
//!     DynColors::Ansi(AnsiColors::Blue),
//!     DynColors::Ansi(AnsiColors::Default),
//!     DynColors::Ansi(AnsiColors::BrightBlue)
//! ];
//!
//! let art: AsciiArt = AsciiArt::new(ASCII, colors.as_slice(), true);
//!
//! for line in art {
//!     println!("{}", line);
//! }
//! ```
//!

use owo_colors::AnsiColors;
use owo_colors::Styled;
use owo_colors::DynColors;
use owo_colors::OwoColorize;
use owo_colors::Style;
use std::fmt::Write;
use std::str::Chars;

pub struct AsciiArt<'a> {
	content: Box<dyn 'a + Iterator<Item = &'a str>>,
	colors: &'a [DynColors],
	bold: bool,
	start: usize,
	end: usize
}

impl<'a> AsciiArt<'a> {
	pub fn new(input: &'a str, colors: &'a [DynColors], bold: bool) -> AsciiArt<'a> {
		let mut lines: Vec<_> = input.lines().skip_while(|line: &&str| line.is_empty()).collect();

		while let Some(line) = lines.last() {
			if Tokens(line).has_no_solid_tokens() {
				lines.pop();
			} else {
				break;
			}
		}

		let (start, end): (usize, usize) = get_min_start_max_end(&lines);

		AsciiArt {
			content: Box::new(lines.into_iter()),
			colors,
			bold,
			start,
			end
		}
	}
}

fn get_min_start_max_end(lines: &[&str]) -> (usize, usize) {
	lines.iter().map(|line: &&str| {
		let line_start: usize = Tokens(line).leading_spaces();
		let line_end:   usize = Tokens(line).true_length();

		(line_start, line_end)
	}).fold((usize::MAX, 0), |(acc_s, acc_e): (usize, usize), (line_s, line_e): (usize, usize)| {
		(acc_s.min(line_s), acc_e.max(line_e))
	})
}

impl Iterator for AsciiArt<'_> {
	type Item = String;

	fn next(&mut self) -> Option<String> {
		self.content.next().map(|line: &str| Tokens(line).render(self.colors, self.start, self.end, self.bold))
	}
}

#[derive(Clone, Debug, PartialEq, Eq)]
enum Token {
	Color(u32),
	Char(char),
	Space
}

impl std::fmt::Display for Token {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match *self {
			Token::Color(c) => write!(f, "{{{}}}", c),
			Token::Char(c)  => write!(f, "{}", c),
			Token::Space    => write!(f, " "),
		}
	}
}

impl Token {
	fn is_solid(&self) -> bool {
		matches!(*self, Token::Char(_))
	}

	fn is_space(&self) -> bool {
		matches!(*self, Token::Space)
	}

	fn has_zero_width(&self) -> bool {
		matches!(*self, Token::Color(_))
	}
}

#[derive(Clone, Debug)]
struct Tokens<'a>(&'a str);

impl Iterator for Tokens<'_> {
	type Item = Token;

	fn next(&mut self) -> Option<Token> {
		let (s, tok): (&str, Token) = color_token(self.0).or_else(|| space_token(self.0)).or_else(|| char_token(self.0))?;
		self.0 = s;

		Some(tok)
	}
}

impl<'a> Tokens<'a> {
	fn has_no_solid_tokens(&mut self) -> bool {
		for token in self {
			if token.is_solid() {
				return false;
			}
		}

		true
	}

	fn true_length(&mut self) -> usize {
		let mut last_non_space: usize = 0;
		let mut last:           usize = 0;

		for token in self {
			if token.has_zero_width() {
				continue;
			}

			last += 1;

			if !token.is_space() {
				last_non_space = last;
			}
		}

		last_non_space
	}

	fn leading_spaces(&mut self) -> usize {
		self.take_while(|token: &Token| !token.is_solid()).filter(Token::is_space).count()
	}

	fn truncate(self, mut start: usize, end: usize) -> impl 'a + Iterator<Item = Token> {
		assert!(start <= end);
		let mut width: usize = end - start;

		self.filter(move |token: &Token| {
			if (start > 0) && !token.has_zero_width() {
				start -= 1;
				return false;
			}

			true
		}).take_while(move |token: &Token| {
			if width == 0 {
				return false;
			}

			if !token.has_zero_width() {
				width -= 1;
			}

			true
		})
	}

	fn render(self, colors: &[DynColors], start: usize, end: usize, bold: bool) -> String {
		assert!(start <= end);

		let mut width: usize = end - start;
		let mut colored_segment: String = String::new();
		let mut whole_string: String = String::new();
		let mut color: &DynColors = &DynColors::Ansi(AnsiColors::Default);

		self.truncate(start, end).for_each(|token: Token| {
			match token {
				Token::Char(chr)  => {
					width = width.saturating_sub(1);
					colored_segment.push(chr);
				},

				Token::Color(col) => {
					add_styled_segment(&mut whole_string, &colored_segment, *color, bold);

					colored_segment = String::new();
					color           = colors.get(col as usize).unwrap_or(&DynColors::Ansi(AnsiColors::Default));
				},

				Token::Space      => {
					width = width.saturating_sub(1);
					colored_segment.push(' ')
				}
			};
		});

		add_styled_segment(&mut whole_string, &colored_segment, *color, bold);
		(0..width).for_each(|_| whole_string.push(' '));

		whole_string
	}
}

fn succeed_when<I>(predicate: impl FnOnce(I) -> bool) -> impl FnOnce(I) -> Option<()> {
	|input: I| {
		if predicate(input) {
			Some(())
		} else {
			None
		}
	}
}

fn add_styled_segment(base: &mut String, segment: &str, color: DynColors, bold: bool) -> () {
	let mut style: Style = Style::new().color(color);

	if bold {
		style = style.bold();
	}

	let formatted_segment: Styled<&&str> = segment.style(style);
	let _ = write!(base, "{}", formatted_segment);
}

type ParseResult<'a, R> = Option<(&'a str, R)>;

fn token<R>(s: &str, predicate: impl FnOnce(char) -> Option<R>) -> ParseResult<R> {
	let mut chars: Chars = s.chars();

	let token: char = chars.next()?;
	let result: R = predicate(token)?;

	Some((chars.as_str(), result))
}

fn color_token(s: &str) -> ParseResult<Token> {
	let (s, _): (&str, ())            = token(s, succeed_when(|c: char| c == '{'))?;
	let (s, color_index): (&str, u32) = token(s, |c: char| c.to_digit(10))?;
	let (s, _): (&str, ())            = token(s, succeed_when(|c: char| c == '}'))?;

	Some((s, Token::Color(color_index)))
}

fn space_token(s: &str) -> ParseResult<Token> {
	token(s, succeed_when(|c: char| c == ' ')).map(|(s, _): (&str, _)| (s, Token::Space))
}

fn char_token(s: &str) -> ParseResult<Token> {
	token(s, |c: char| Some(Token::Char(c)))
}