#![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;
#[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 {
num_chars: text.len(),
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> {
if self.finished {
return None;
}
let mut state = SpanIterState::GatheringStyles(GatheringStylesState::ExpectingStartChar);
let mut span_start = None;
let mut span_end = None;
while let Some((idx, c)) = self.chars.next() {
state = match state {
SpanIterState::GatheringStyles(style_state) => match style_state {
GatheringStylesState::ExpectingStartChar => {
span_start = Some(idx);
match c {
c if c == self.start_char => SpanIterState::GatheringStyles(
GatheringStylesState::ExpectingFmtCode,
),
_ => SpanIterState::GatheringText(
GatheringTextState::WaitingForStartChar,
),
}
}
GatheringStylesState::ExpectingFmtCode => {
if let Some(color) = Color::from_char(c) {
self.update_color(color);
span_start = None;
SpanIterState::GatheringStyles(GatheringStylesState::ExpectingStartChar)
} else if let Some(style) = Styles::from_char(c) {
self.update_styles(style);
span_start = None;
SpanIterState::GatheringStyles(GatheringStylesState::ExpectingStartChar)
} else if c == 'r' || c == 'R' {
self.reset_styles();
span_start = None;
SpanIterState::GatheringStyles(GatheringStylesState::ExpectingStartChar)
} else {
SpanIterState::GatheringText(GatheringTextState::WaitingForStartChar)
}
}
},
SpanIterState::GatheringText(text_state) => match text_state {
GatheringTextState::WaitingForStartChar => match c {
c if c == self.start_char => {
span_end = Some(idx);
SpanIterState::GatheringText(GatheringTextState::ExpectingEndChar)
}
_ => state,
},
GatheringTextState::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;
SpanIterState::GatheringText(GatheringTextState::WaitingForStartChar)
}
}
},
}
}
self.finished = true;
span_start.map(|start| self.make_span(start, self.buf.len()))
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Span<'a> {
Styled {
text: &'a str,
color: Color,
styles: Styles,
},
StrikethroughWhitespace {
num_chars: usize,
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 { num_chars, .. } => {
(0..*num_chars).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(num_chars: usize, color: Color, styles: Styles) -> Self {
Span::StrikethroughWhitespace {
num_chars,
color,
styles,
}
}
pub fn new_styled(s: &'a str, color: Color, styles: Styles) -> Self {
Span::Styled {
text: s,
color,
styles,
}
}
}
#[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,
})
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use std::vec;
use std::vec::Vec;
fn spans(s: &str) -> Vec<Span> {
SpanIter::new(s).collect()
}
fn spans_sc(start_char: char, s: &str) -> Vec<Span> {
SpanIter::new(s).with_start_char(start_char).collect()
}
mod fake_codes {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn no_formatting_code() {
let s = "this has no formatting codes";
assert_eq!(
spans(s),
vec![Span::new_plain("this has no formatting codes")]
);
}
#[test]
fn fake_code_at_start() {
let s = "§this has no formatting codes";
assert_eq!(
spans(s),
vec![Span::new_plain("§this has no formatting codes")]
);
}
#[test]
fn fake_code_space_at_start() {
let s = "§ this has no formatting codes";
assert_eq!(
spans(s),
vec![Span::new_plain("§ this has no formatting codes")]
);
}
#[test]
fn fake_code_at_end() {
let s = "this has no formatting codes§";
assert_eq!(
spans(s),
vec![Span::new_plain("this has no formatting codes§")]
);
}
#[test]
fn fake_code_space_at_end() {
let s = "this has no formatting codes §";
assert_eq!(
spans(s),
vec![Span::new_plain("this has no formatting codes §")]
);
}
#[test]
fn fake_code_middle() {
let s = "this ha§s no formatting codes";
assert_eq!(
spans(s),
vec![Span::new_plain("this ha§s no formatting codes")]
);
}
#[test]
fn fake_code_space_middle() {
let s = "this has no § formatting codes";
assert_eq!(
spans(s),
vec![Span::new_plain("this has no § formatting codes")]
);
}
#[test]
fn a_bunch_of_fakes() {
let s = "§§§§§this has no format§ting codes§";
assert_eq!(
spans(s),
vec![Span::new_plain("§§§§§this has no format§ting codes§")]
);
}
}
mod custom_start_char {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn using_ampersand() {
let s = "&4this will be dark red";
assert_eq!(
spans_sc('&', s),
vec![Span::new_styled(
"this will be dark red",
Color::DarkRed,
Styles::empty()
)]
);
}
#[test]
fn multiple_styles() {
let s = "&1&e&d<his will be light purple and bold &o&a&e&a&mand this \
will be green and strikethrough";
assert_eq!(
spans_sc('&', s),
vec![
Span::new_styled(
"this will be light purple and bold ",
Color::LightPurple,
Styles::BOLD
),
Span::new_styled(
"and this will be green and strikethrough",
Color::Green,
Styles::STRIKETHROUGH
)
]
);
}
#[test]
fn supports_uppercase_style_codes() {
let s = "&5&m &6>&7&l&6&l>&6&l[&5&l&oPurple &8&l&oPrison&6&l]&6&l<&6<&5&m \
&R &7 (&4!&7) &e&lSERVER HAS &D&LRESET! &7(&4!&7)";
assert_eq!(
spans_sc('&', s),
vec![
Span::new_strikethrough_whitespace(
18,
Color::DarkPurple,
Styles::STRIKETHROUGH
),
Span::new_styled(">", Color::Gold, Styles::empty()),
Span::new_styled(">", Color::Gold, Styles::BOLD),
Span::new_styled("[", Color::Gold, Styles::BOLD),
Span::new_styled("Purple ", Color::DarkPurple, Styles::BOLD | Styles::ITALIC),
Span::new_styled("Prison", Color::DarkGray, Styles::BOLD | Styles::ITALIC),
Span::new_styled("]", Color::Gold, Styles::BOLD),
Span::new_styled("<", Color::Gold, Styles::BOLD),
Span::new_styled("<", Color::Gold, Styles::empty()),
Span::new_strikethrough_whitespace(
21,
Color::DarkPurple,
Styles::STRIKETHROUGH
),
Span::new_plain(" "),
Span::new_styled(" (", Color::Gray, Styles::empty()),
Span::new_styled("!", Color::DarkRed, Styles::empty()),
Span::new_styled(") ", Color::Gray, Styles::empty()),
Span::new_styled("SERVER HAS ", Color::Yellow, Styles::BOLD),
Span::new_styled("RESET! ", Color::LightPurple, Styles::BOLD),
Span::new_styled("(", Color::Gray, Styles::empty()),
Span::new_styled("!", Color::DarkRed, Styles::empty()),
Span::new_styled(")", Color::Gray, Styles::empty()),
]
);
}
}
#[test]
fn dark_red() {
let s = "§4this will be dark red";
assert_eq!(
spans(s),
vec![Span::new_styled(
"this will be dark red",
Color::DarkRed,
Styles::empty()
)]
);
}
#[test]
fn dark_blue() {
let s = "§1this will be dark blue";
assert_eq!(
spans(s),
vec![Span::new_styled(
"this will be dark blue",
Color::DarkBlue,
Styles::empty()
)]
);
}
#[test]
fn aqua() {
let s = "§1§bthis will be aqua";
assert_eq!(
spans(s),
vec![Span::new_styled(
"this will be aqua",
Color::Aqua,
Styles::empty()
)]
);
}
#[test]
fn light_purple_and_bold() {
let s = "§1§e§d§lthis will be light purple and bold";
assert_eq!(
spans(s),
vec![Span::new_styled(
"this will be light purple and bold",
Color::LightPurple,
Styles::BOLD
)]
);
}
#[test]
fn multiple_styles() {
let s = "§1§e§d§lthis will be light purple and bold §o§a§e§a§mand this \
will be green and strikethrough";
assert_eq!(
spans(s),
vec![
Span::new_styled(
"this will be light purple and bold ",
Color::LightPurple,
Styles::BOLD
),
Span::new_styled(
"and this will be green and strikethrough",
Color::Green,
Styles::STRIKETHROUGH
)
]
);
}
#[test]
fn multiple_styles_no_colors() {
let s = "§lthis will be bold §o§mand this will be bold, italic, and strikethrough";
assert_eq!(
spans(s),
vec![
Span::new_styled("this will be bold ", Color::White, Styles::BOLD),
Span::new_styled(
"and this will be bold, italic, and strikethrough",
Color::White,
Styles::BOLD | Styles::ITALIC | Styles::STRIKETHROUGH
)
]
);
}
#[test]
fn colors_and_styles_at_end() {
let s = "basic stuff but then§o§a§e§a§m";
assert_eq!(spans(s), vec![Span::new_plain("basic stuff but then")]);
}
#[test]
fn multiline_message() {
let s = "§8Welcome to §6§lAmazing Minecraft Server\n§8§oYour hub for §d§op2w §8§ogameplay!";
assert_eq!(
spans(s),
vec![
Span::new_styled("Welcome to ", Color::DarkGray, Styles::empty()),
Span::new_styled("Amazing Minecraft Server\n", Color::Gold, Styles::BOLD),
Span::new_styled("Your hub for ", Color::DarkGray, Styles::ITALIC),
Span::new_styled("p2w ", Color::LightPurple, Styles::ITALIC),
Span::new_styled("gameplay!", Color::DarkGray, Styles::ITALIC)
]
);
}
#[test]
fn real_motd() {
let s = " §7§l<§a§l+§7§l>§8§l§m-----§8§l[ §a§lMine§7§lSuperior§a§l Network§8§l ]§8§l§m-----§7§l<§a§l+§7§l>\n\
§a§l§n1.7-1.16 SUPPORT§r §7§l| §a§lSITE§7§l:§a§l§nwww.minesuperior.com";
assert_eq!(
spans(s),
vec![
Span::new_plain(" "),
Span::new_styled("<", Color::Gray, Styles::BOLD),
Span::new_styled("+", Color::Green, Styles::BOLD),
Span::new_styled(">", Color::Gray, Styles::BOLD),
Span::new_styled(
"-----",
Color::DarkGray,
Styles::BOLD | Styles::STRIKETHROUGH
),
Span::new_styled("[ ", Color::DarkGray, Styles::BOLD),
Span::new_styled("Mine", Color::Green, Styles::BOLD),
Span::new_styled("Superior", Color::Gray, Styles::BOLD),
Span::new_styled(" Network", Color::Green, Styles::BOLD),
Span::new_styled(" ]", Color::DarkGray, Styles::BOLD),
Span::new_styled(
"-----",
Color::DarkGray,
Styles::BOLD | Styles::STRIKETHROUGH
),
Span::new_styled("<", Color::Gray, Styles::BOLD),
Span::new_styled("+", Color::Green, Styles::BOLD),
Span::new_styled(">\n", Color::Gray, Styles::BOLD),
Span::new_styled(
"1.7-1.16 SUPPORT",
Color::Green,
Styles::BOLD | Styles::UNDERLINED
),
Span::Plain(" "),
Span::new_styled("| ", Color::Gray, Styles::BOLD),
Span::new_styled("SITE", Color::Green, Styles::BOLD),
Span::new_styled(":", Color::Gray, Styles::BOLD),
Span::new_styled(
"www.minesuperior.com",
Color::Green,
Styles::BOLD | Styles::UNDERLINED
)
]
);
}
#[test]
fn supports_uppercase_style_codes() {
let s = "§5§m §6>§7§l§6§l>§6§l[§5§l§oPurple §8§l§oPrison§6§l]§6§l<§6<§5§m \
§R §7 (§4!§7) §e§lSERVER HAS §D§LRESET! §7(§4!§7)";
assert_eq!(
spans(s),
vec![
Span::new_strikethrough_whitespace(18, Color::DarkPurple, Styles::STRIKETHROUGH),
Span::new_styled(">", Color::Gold, Styles::empty()),
Span::new_styled(">", Color::Gold, Styles::BOLD),
Span::new_styled("[", Color::Gold, Styles::BOLD),
Span::new_styled("Purple ", Color::DarkPurple, Styles::BOLD | Styles::ITALIC),
Span::new_styled("Prison", Color::DarkGray, Styles::BOLD | Styles::ITALIC),
Span::new_styled("]", Color::Gold, Styles::BOLD),
Span::new_styled("<", Color::Gold, Styles::BOLD),
Span::new_styled("<", Color::Gold, Styles::empty()),
Span::new_strikethrough_whitespace(21, Color::DarkPurple, Styles::STRIKETHROUGH),
Span::new_plain(" "),
Span::new_styled(" (", Color::Gray, Styles::empty()),
Span::new_styled("!", Color::DarkRed, Styles::empty()),
Span::new_styled(") ", Color::Gray, Styles::empty()),
Span::new_styled("SERVER HAS ", Color::Yellow, Styles::BOLD),
Span::new_styled("RESET! ", Color::LightPurple, Styles::BOLD),
Span::new_styled("(", Color::Gray, Styles::empty()),
Span::new_styled("!", Color::DarkRed, Styles::empty()),
Span::new_styled(")", Color::Gray, Styles::empty()),
]
);
}
#[test]
fn avoids_incorrect_whitespace_strikethrough() {
let s = "§f§b§lMINE§6§lHEROES §7- §astore.mineheroes.net§a §2§l[75% Sale]\n§b§lSKYBLOCK §f§l+ §2§lKRYPTON §f§lRESET! §f§l- §6§lNEW FALL CRATE";
assert_eq!(
spans(s),
vec![
Span::new_styled("MINE", Color::Aqua, Styles::BOLD),
Span::new_styled("HEROES ", Color::Gold, Styles::BOLD),
Span::new_styled("- ", Color::Gray, Styles::empty()),
Span::new_styled("store.mineheroes.net", Color::Green, Styles::empty()),
Span::new_styled(" ", Color::Green, Styles::empty()),
Span::new_styled("[75% Sale]\n", Color::DarkGreen, Styles::BOLD),
Span::new_styled("SKYBLOCK ", Color::Aqua, Styles::BOLD),
Span::new_styled("+ ", Color::White, Styles::BOLD),
Span::new_styled("KRYPTON ", Color::DarkGreen, Styles::BOLD),
Span::new_styled("RESET! ", Color::White, Styles::BOLD),
Span::new_styled("- ", Color::White, Styles::BOLD),
Span::new_styled("NEW FALL CRATE", Color::Gold, Styles::BOLD)
]
);
}
}