#![no_std]
#![deny(missing_docs)]
#![deny(unused_must_use)]
#[cfg(test)]
extern crate std;
use core::str::CharIndices;
use bitflags::bitflags;
#[cfg(feature = "color-print")]
mod color_print;
#[cfg(feature = "color-print")]
pub use color_print::PrintSpanColored;
pub trait SpanExt {
fn span_iter(&self) -> SpanIter;
}
impl<T: AsRef<str>> SpanExt for T {
fn span_iter(&self) -> SpanIter {
SpanIter::new(self.as_ref())
}
}
#[derive(Debug, Clone)]
pub struct SpanIter<'a> {
buf: &'a str,
chars: CharIndices<'a>,
start_char: char,
color: Color,
styles: Styles,
finished: bool,
}
impl<'a> SpanIter<'a> {
pub fn new(s: &'a str) -> Self {
Self {
buf: s,
chars: s.char_indices(),
start_char: '§',
color: Color::White,
styles: Styles::default(),
finished: false,
}
}
pub fn with_start_char(mut self, c: char) -> Self {
self.start_char = c;
self
}
pub fn set_start_char(&mut self, c: char) {
self.start_char = c;
}
fn update_color(&mut self, color: Color) {
self.color = color;
self.styles = Styles::empty();
}
fn update_styles(&mut self, styles: Styles) {
self.styles.insert(styles);
}
fn reset_styles(&mut self) {
self.color = Color::White;
self.styles = Styles::empty();
}
fn make_span(&self, start: usize, end: usize) -> Span<'a> {
if self.color == Color::White && self.styles.is_empty() {
Span::Plain(&self.buf[start..end])
} else {
let text = &self.buf[start..end];
if text.chars().all(|c| c.is_ascii_whitespace())
&& self.styles.contains(Styles::STRIKETHROUGH)
{
Span::StrikethroughWhitespace {
text,
color: self.color,
styles: self.styles,
}
} else {
Span::Styled {
text,
color: self.color,
styles: self.styles,
}
}
}
}
}
#[derive(Debug, Copy, Clone)]
enum SpanIterState {
GatheringStyles(GatheringStylesState),
GatheringText(GatheringTextState),
}
#[derive(Debug, Copy, Clone)]
enum GatheringStylesState {
ExpectingStartChar,
ExpectingFmtCode,
}
#[derive(Debug, Copy, Clone)]
enum GatheringTextState {
WaitingForStartChar,
ExpectingEndChar,
}
impl<'a> Iterator for SpanIter<'a> {
type Item = Span<'a>;
fn next(&mut self) -> Option<Self::Item> {
use GatheringStylesState::*;
use GatheringTextState::*;
use SpanIterState::*;
if self.finished {
return None;
}
let mut state = GatheringStyles(ExpectingStartChar);
let mut span_start = None;
let mut span_end = None;
while let Some((idx, c)) = self.chars.next() {
state = match state {
GatheringStyles(style_state) => match style_state {
ExpectingStartChar => {
span_start = Some(idx);
match c {
c if c == self.start_char => GatheringStyles(ExpectingFmtCode),
_ => GatheringText(WaitingForStartChar),
}
}
ExpectingFmtCode => {
if let Some(color) = Color::from_char(c) {
self.update_color(color);
span_start = None;
GatheringStyles(ExpectingStartChar)
} else if let Some(style) = Styles::from_char(c) {
self.update_styles(style);
span_start = None;
GatheringStyles(ExpectingStartChar)
} else if c == 'r' || c == 'R' {
self.reset_styles();
span_start = None;
GatheringStyles(ExpectingStartChar)
} else {
GatheringText(WaitingForStartChar)
}
}
},
GatheringText(text_state) => match text_state {
WaitingForStartChar => match c {
c if c == self.start_char => {
span_end = Some(idx);
GatheringText(ExpectingEndChar)
}
_ => state,
},
ExpectingEndChar => {
if let Some(color) = Color::from_char(c) {
let span = self.make_span(span_start.unwrap(), span_end.unwrap());
self.update_color(color);
return Some(span);
} else if let Some(style) = Styles::from_char(c) {
let span = self.make_span(span_start.unwrap(), span_end.unwrap());
self.update_styles(style);
return Some(span);
} else if c == 'r' || c == 'R' {
let span = self.make_span(span_start.unwrap(), span_end.unwrap());
self.reset_styles();
return Some(span);
} else {
span_end = None;
GatheringText(WaitingForStartChar)
}
}
},
}
}
self.finished = true;
span_start.map(|start| self.make_span(start, self.buf.len()))
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Span<'a> {
Styled {
text: &'a str,
color: Color,
styles: Styles,
},
StrikethroughWhitespace {
text: &'a str,
color: Color,
styles: Styles,
},
Plain(&'a str),
}
impl<'a> core::fmt::Display for Span<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match self {
Span::Styled { text, .. } => f.write_str(text),
Span::StrikethroughWhitespace { text, .. } => {
(0..text.len()).try_for_each(|_| f.write_str("-"))
}
Span::Plain(text) => f.write_str(text),
}
}
}
impl<'a> Span<'a> {
pub fn new_plain(s: &'a str) -> Self {
Span::Plain(s)
}
pub fn new_strikethrough_whitespace(s: &'a str, color: Color, styles: Styles) -> Self {
Span::StrikethroughWhitespace {
text: s,
color,
styles,
}
}
pub fn new_styled(s: &'a str, color: Color, styles: Styles) -> Self {
Span::Styled {
text: s,
color,
styles,
}
}
#[cfg(feature = "color-print")]
pub fn wrap_colored(self) -> PrintSpanColored<'a> {
PrintSpanColored::from(self)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
#[allow(missing_docs)]
pub enum Color {
Black,
DarkBlue,
DarkGreen,
DarkAqua,
DarkRed,
DarkPurple,
Gold,
Gray,
DarkGray,
Blue,
Green,
Aqua,
Red,
LightPurple,
Yellow,
White,
}
impl Default for Color {
fn default() -> Self {
Color::White
}
}
impl Color {
pub fn from_char(c: char) -> Option<Self> {
Some(match c {
'0' => Color::Black,
'1' => Color::DarkBlue,
'2' => Color::DarkGreen,
'3' => Color::DarkAqua,
'4' => Color::DarkRed,
'5' => Color::DarkPurple,
'6' => Color::Gold,
'7' => Color::Gray,
'8' => Color::DarkGray,
'9' => Color::DarkBlue,
'a' | 'A' => Color::Green,
'b' | 'B' => Color::Aqua,
'c' | 'C' => Color::Red,
'd' | 'D' => Color::LightPurple,
'e' | 'E' => Color::Yellow,
'f' | 'F' => Color::White,
_ => return None,
})
}
pub const fn foreground_hex_str(&self) -> &'static str {
match self {
Color::Black => "#000000",
Color::DarkBlue => "#0000aa",
Color::DarkGreen => "#00aa00",
Color::DarkAqua => "#00aaaa",
Color::DarkRed => "#aa0000",
Color::DarkPurple => "#aa00aa",
Color::Gold => "#ffaa00",
Color::Gray => "#aaaaaa",
Color::DarkGray => "#555555",
Color::Blue => "#5555ff",
Color::Green => "#55ff55",
Color::Aqua => "#55ffff",
Color::Red => "#ff5555",
Color::LightPurple => "#ff55ff",
Color::Yellow => "#ffff55",
Color::White => "#ffffff",
}
}
pub const fn background_hex_str(&self) -> &'static str {
match self {
Color::Black => "#000000",
Color::DarkBlue => "#00002a",
Color::DarkGreen => "#002a00",
Color::DarkAqua => "#002a2a",
Color::DarkRed => "#2a0000",
Color::DarkPurple => "#2a002a",
Color::Gold => "#2a2a00",
Color::Gray => "#2a2a2a",
Color::DarkGray => "#151515",
Color::Blue => "#15153f",
Color::Green => "#153f15",
Color::Aqua => "#153f3f",
Color::Red => "#3f1515",
Color::LightPurple => "#3f153f",
Color::Yellow => "#3f3f15",
Color::White => "#3f3f3f",
}
}
pub const fn foreground_rgb(&self) -> (u8, u8, u8) {
match self {
Color::Black => (0, 0, 0),
Color::DarkBlue => (0, 0, 170),
Color::DarkGreen => (0, 170, 0),
Color::DarkAqua => (0, 170, 170),
Color::DarkRed => (170, 0, 0),
Color::DarkPurple => (170, 0, 170),
Color::Gold => (255, 170, 0),
Color::Gray => (170, 170, 170),
Color::DarkGray => (85, 85, 85),
Color::Blue => (85, 85, 255),
Color::Green => (85, 255, 85),
Color::Aqua => (85, 255, 255),
Color::Red => (255, 85, 85),
Color::LightPurple => (255, 85, 255),
Color::Yellow => (255, 255, 85),
Color::White => (255, 255, 255),
}
}
pub const fn background_rgb(&self) -> (u8, u8, u8) {
match self {
Color::Black => (0, 0, 0),
Color::DarkBlue => (0, 0, 42),
Color::DarkGreen => (0, 42, 0),
Color::DarkAqua => (0, 42, 42),
Color::DarkRed => (42, 0, 0),
Color::DarkPurple => (42, 0, 42),
Color::Gold => (42, 42, 0),
Color::Gray => (42, 42, 42),
Color::DarkGray => (21, 21, 21),
Color::Blue => (21, 21, 63),
Color::Green => (21, 63, 21),
Color::Aqua => (21, 63, 63),
Color::Red => (63, 21, 21),
Color::LightPurple => (63, 21, 63),
Color::Yellow => (63, 63, 21),
Color::White => (63, 63, 63),
}
}
}
bitflags! {
#[derive(Default)]
pub struct Styles: u32 {
const RANDOM = 0b00000001;
const BOLD = 0b00000010;
const STRIKETHROUGH = 0b00000100;
const UNDERLINED = 0b00001000;
const ITALIC = 0b00010000;
}
}
impl Styles {
pub fn from_char(c: char) -> Option<Self> {
Some(match c {
'k' | 'K' => Styles::RANDOM,
'l' | 'L' => Styles::BOLD,
'm' | 'M' => Styles::STRIKETHROUGH,
'n' | 'N' => Styles::UNDERLINED,
'o' | 'O' => Styles::ITALIC,
_ => return None,
})
}
}