minecraft-formatting 0.1.0

Library to parse minecraft formatting codes and print them to a terminal
Documentation
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,
            // No formatting codes left in input
            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
            // Skip § symbol
            input.get((symbol_index + 2)..)
            // Try to parse char as a formatting code
            .and_then(|code_slice| {
                // Getting first char from slice
                code_slice.chars().next().and_then(|first| first.try_into().ok())
            }) {
            // Valid code
            Some(code_type) => {
                if symbol_index != 0 {
                    // Use text_buffer incase there is somting left over from previous loops
                    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();
                    }
                }
            },
            // Invalid code or there is a symbol at the end of input
            None => {
                text_buffer.push_str(&input[..(symbol_index+2)]);
                input = &input[(symbol_index + 2)..];
            },
        };
    }
    // If there is text left in the buffer it still needs to be emited
    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()
            }]
        );
    }
}