use bitflags::bitflags;
use crossterm::execute;
use crossterm::style::{Color, Print, SetForegroundColor, SetAttributes, Attributes};
use std::convert::{TryFrom, TryInto};
use std::str::FromStr;
use std::io::{stdout, Write};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MCColor {
Black,
DarkBlue,
DarkGreen,
DarkAqua,
DarkRed,
DarkPurple,
Gold,
Gray,
DarkGray,
Blue,
Green,
Aqua,
Red,
LightPurple,
Yellow,
White,
}
impl MCColor {
pub fn to_crossterm(&self) -> Color {
match self {
Self::Black => Color::Black,
Self::DarkBlue => Color::DarkBlue,
Self::DarkGreen => Color::DarkGreen,
Self::DarkAqua => Color::DarkCyan,
Self::DarkRed => Color::DarkRed,
Self::DarkPurple => Color::DarkMagenta,
Self::Gold => Color::DarkYellow,
Self::Gray => Color::Grey,
Self::DarkGray => Color::DarkGrey,
Self::Blue => Color::Blue,
Self::Green => Color::Green,
Self::Aqua => Color::Cyan,
Self::Red => Color::Black,
Self::LightPurple => Color::Magenta,
Self::Yellow => Color::Yellow,
Self::White => Color::White,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Code {
Color(MCColor),
Effect(Effects),
Reset,
}
impl FromStr for Code {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut chars = s.chars();
if let Some('§') = chars.next() {
match chars.next().ok_or("No other char")? {
'0' => Ok(Code::Color(MCColor::Black)),
'1' => Ok(Code::Color(MCColor::DarkBlue)),
'2' => Ok(Code::Color(MCColor::DarkGreen)),
'3' => Ok(Code::Color(MCColor::DarkAqua)),
'4' => Ok(Code::Color(MCColor::DarkRed)),
'5' => Ok(Code::Color(MCColor::DarkPurple)),
'6' => Ok(Code::Color(MCColor::Gold)),
'7' => Ok(Code::Color(MCColor::Gray)),
'8' => Ok(Code::Color(MCColor::DarkGray)),
'9' => Ok(Code::Color(MCColor::Blue)),
'a' => Ok(Code::Color(MCColor::Green)),
'b' => Ok(Code::Color(MCColor::Aqua)),
'c' => Ok(Code::Color(MCColor::Red)),
'd' => Ok(Code::Color(MCColor::LightPurple)),
'e' => Ok(Code::Color(MCColor::Yellow)),
'f' => Ok(Code::Color(MCColor::White)),
'k' => Ok(Code::Effect(Effects::OBFUSCATED)),
'l' => Ok(Code::Effect(Effects::BOLD)),
'm' => Ok(Code::Effect(Effects::STRIKETHROUGH)),
'n' => Ok(Code::Effect(Effects::UNDERLINE)),
'o' => Ok(Code::Effect(Effects::ITALIC)),
'r' => Ok(Code::Reset),
_ => Err("Code not recognized"),
}
} else {
Err("Missing starting '§'")
}
}
}
impl TryFrom<char> for Code {
type Error = &'static str;
fn try_from(value: char) -> Result<Self, Self::Error> {
match value {
'0' => Ok(Code::Color(MCColor::Black)),
'1' => Ok(Code::Color(MCColor::DarkBlue)),
'2' => Ok(Code::Color(MCColor::DarkGreen)),
'3' => Ok(Code::Color(MCColor::DarkAqua)),
'4' => Ok(Code::Color(MCColor::DarkRed)),
'5' => Ok(Code::Color(MCColor::DarkPurple)),
'6' => Ok(Code::Color(MCColor::Gold)),
'7' => Ok(Code::Color(MCColor::Gray)),
'8' => Ok(Code::Color(MCColor::DarkGray)),
'9' => Ok(Code::Color(MCColor::Blue)),
'a' => Ok(Code::Color(MCColor::Green)),
'b' => Ok(Code::Color(MCColor::Aqua)),
'c' => Ok(Code::Color(MCColor::Red)),
'd' => Ok(Code::Color(MCColor::LightPurple)),
'e' => Ok(Code::Color(MCColor::Yellow)),
'f' => Ok(Code::Color(MCColor::White)),
'k' => Ok(Code::Effect(Effects::OBFUSCATED)),
'l' => Ok(Code::Effect(Effects::BOLD)),
'm' => Ok(Code::Effect(Effects::STRIKETHROUGH)),
'n' => Ok(Code::Effect(Effects::UNDERLINE)),
'o' => Ok(Code::Effect(Effects::ITALIC)),
'r' => Ok(Code::Reset),
_ => Err("Code not recognized"),
}
}
}
bitflags! {
pub struct Effects: u8 {
const OBFUSCATED = 1 << 0;
const BOLD = 1 << 1;
const STRIKETHROUGH = 1 << 2;
const UNDERLINE = 1 << 3;
const ITALIC = 1 << 4;
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Span {
pub text: String,
pub color: Option<MCColor>,
pub effects: Effects,
}
impl Span {
pub fn write_out(&self, mut out: impl Write) {
let res = execute! {
out,
SetForegroundColor(self.color.map(|mc_color| mc_color.to_crossterm()).unwrap_or(Color::Reset)),
Print(&self.text),
SetForegroundColor(Color::Reset),
};
res.unwrap();
}
pub fn print(&self) {
self.write_out(stdout())
}
}
pub fn formatting_tokenize(mut input: &str) -> Vec<Span> {
let mut current_color: Option<MCColor> = None;
let mut current_effects = Effects::empty();
let mut output = Vec::new();
let mut text_buffer = String::new();
while !input.is_empty() {
dbg!(&text_buffer);
let symbol_index = match input.find('§') {
Some(symbol_index) => symbol_index,
None => {
text_buffer.push_str(input);
output.push(Span {
text: std::mem::take(&mut text_buffer),
color: current_color,
effects: current_effects,
});
break;
}
};
dbg!(&symbol_index);
dbg!(&input[..symbol_index]);
match
input.get((symbol_index + 2)..)
.and_then(|code_slice| {
code_slice.chars().next().and_then(|first| first.try_into().ok())
}) {
Some(code_type) => {
if symbol_index != 0 {
text_buffer.push_str(&input[..symbol_index]);
output.push( Span { text: std::mem::take(&mut text_buffer), color: current_color, effects: current_effects});
}
input = &input[(symbol_index + 3)..];
match code_type {
Code::Color(c) => current_color = Some(c),
Code::Effect(e) => current_effects |= e,
Code::Reset => {
current_color = None;
current_effects = Effects::empty();
}
}
},
None => {
text_buffer.push_str(&input[..(symbol_index+2)]);
input = &input[(symbol_index + 2)..];
},
};
}
if !text_buffer.is_empty() {
output.push(Span {
text: std::mem::take(&mut text_buffer),
color: current_color,
effects: current_effects,
});
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nothing() {
assert_eq!(
formatting_tokenize("Hello, World!"),
vec![Span {
text: String::from("Hello, World!"),
color: None,
effects: Effects::empty()
}]
);
}
#[test]
fn print_out() {
for i in &[
Span {
text: String::from("Plain"),
color: None,
effects: Effects::empty(),
},
Span {
text: String::from("then red"),
color: Some(MCColor::Red),
effects: Effects::empty(),
},
Span {
text: String::from("blue"),
color: Some(MCColor::Blue),
effects: Effects::empty(),
},
Span {
text: String::from("dark purple"),
color: Some(MCColor::DarkPurple),
effects: Effects::empty(),
},
Span {
text: String::from("gold"),
color: Some(MCColor::Gold),
effects: Effects::empty(),
},
] {
i.print();
}
}
#[test]
fn basic() {
assert_eq!(
formatting_tokenize("Plain§cthen red"),
vec![
Span {
text: String::from("Plain"),
color: None,
effects: Effects::empty()
},
Span {
text: String::from("then red"),
color: Some(MCColor::Red),
effects: Effects::empty()
}
]
);
}
#[test]
fn code_at_start() {
assert_eq!(
formatting_tokenize("§0Black"),
vec![Span {
text: String::from("Black"),
color: Some(MCColor::Black),
effects: Effects::empty()
}]
);
}
#[test]
fn mutiple_codes() {
assert_eq!(
formatting_tokenize("Plain§0§lBlack and Bold"),
vec![
Span {
text: String::from("Plain"),
color: None,
effects: Effects::empty()
},
Span {
text: String::from("Black and Bold"),
color: Some(MCColor::Black),
effects: Effects::BOLD
}
]
);
}
#[test]
fn two_effects() {
assert_eq!(
formatting_tokenize("§n§lUnder Bold"),
vec![Span {
text: String::from("Under Bold"),
color: None,
effects: Effects::UNDERLINE | Effects::BOLD
}]
);
}
#[test]
fn cascading() {
assert_eq!(formatting_tokenize("Plain§nUnderlined§eUnder Yellow§oUnder Italic Yellow§9Under Italic Blue§rPlain again"),
vec![Span { text: String::from("Plain"), color: None, effects: Effects::empty() },
Span { text: String::from("Underlined"), color: None, effects: Effects::UNDERLINE },
Span { text: String::from("Under Yellow"), color: Some(MCColor::Yellow), effects: Effects::UNDERLINE },
Span { text: String::from("Under Italic Yellow"), color: Some(MCColor::Yellow), effects: Effects::UNDERLINE | Effects::ITALIC },
Span { text: String::from("Under Italic Blue"), color: Some(MCColor::Blue), effects: Effects::UNDERLINE | Effects::ITALIC },
Span { text: String::from("Plain again"), color: None, effects: Effects::empty() }]);
}
#[test]
fn ignore_embedded_noncodes() {
assert_eq!(
formatting_tokenize("I Like §§§ alot!"),
vec![Span {
text: String::from("I Like §§§ alot!"),
color: None,
effects: Effects::empty()
}]
);
}
#[test]
fn trailing_noncodes() {
assert_eq!(
formatting_tokenize("-> §"),
vec![Span {
text: String::from("-> §"),
color: None,
effects: Effects::empty()
}]
);
}
}