minecraft_formatting/
lib.rs

1use bitflags::bitflags;
2use crossterm::execute;
3use crossterm::style::{Color, Print, SetForegroundColor, SetAttributes, Attributes};
4use std::convert::{TryFrom, TryInto};
5use std::str::FromStr;
6use std::io::{stdout, Write};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum MCColor {
10    Black,
11    DarkBlue,
12    DarkGreen,
13    DarkAqua,
14    DarkRed,
15    DarkPurple,
16    Gold,
17    Gray,
18    DarkGray,
19    Blue,
20    Green,
21    Aqua,
22    Red,
23    LightPurple,
24    Yellow,
25    White,
26}
27
28impl MCColor {
29    pub fn to_crossterm(&self) -> Color {
30        match self {
31            Self::Black => Color::Black,
32            Self::DarkBlue => Color::DarkBlue,
33            Self::DarkGreen => Color::DarkGreen,
34            Self::DarkAqua => Color::DarkCyan,
35            Self::DarkRed => Color::DarkRed,
36            Self::DarkPurple => Color::DarkMagenta,
37            Self::Gold => Color::DarkYellow,
38            Self::Gray => Color::Grey,
39            Self::DarkGray => Color::DarkGrey,
40            Self::Blue => Color::Blue,
41            Self::Green => Color::Green,
42            Self::Aqua => Color::Cyan,
43            Self::Red => Color::Black,
44            Self::LightPurple => Color::Magenta,
45            Self::Yellow => Color::Yellow,
46            Self::White => Color::White,
47        }
48    }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum Code {
53    Color(MCColor),
54    Effect(Effects),
55    Reset,
56}
57
58impl FromStr for Code {
59    type Err = &'static str;
60    fn from_str(s: &str) -> Result<Self, Self::Err> {
61        let mut chars = s.chars();
62        if let Some('§') = chars.next() {
63            match chars.next().ok_or("No other char")? {
64                '0' => Ok(Code::Color(MCColor::Black)),
65                '1' => Ok(Code::Color(MCColor::DarkBlue)),
66                '2' => Ok(Code::Color(MCColor::DarkGreen)),
67                '3' => Ok(Code::Color(MCColor::DarkAqua)),
68                '4' => Ok(Code::Color(MCColor::DarkRed)),
69                '5' => Ok(Code::Color(MCColor::DarkPurple)),
70                '6' => Ok(Code::Color(MCColor::Gold)),
71                '7' => Ok(Code::Color(MCColor::Gray)),
72                '8' => Ok(Code::Color(MCColor::DarkGray)),
73                '9' => Ok(Code::Color(MCColor::Blue)),
74                'a' => Ok(Code::Color(MCColor::Green)),
75                'b' => Ok(Code::Color(MCColor::Aqua)),
76                'c' => Ok(Code::Color(MCColor::Red)),
77                'd' => Ok(Code::Color(MCColor::LightPurple)),
78                'e' => Ok(Code::Color(MCColor::Yellow)),
79                'f' => Ok(Code::Color(MCColor::White)),
80                'k' => Ok(Code::Effect(Effects::OBFUSCATED)),
81                'l' => Ok(Code::Effect(Effects::BOLD)),
82                'm' => Ok(Code::Effect(Effects::STRIKETHROUGH)),
83                'n' => Ok(Code::Effect(Effects::UNDERLINE)),
84                'o' => Ok(Code::Effect(Effects::ITALIC)),
85                'r' => Ok(Code::Reset),
86                _ => Err("Code not recognized"),
87            }
88        } else {
89            Err("Missing starting '§'")
90        }
91    }
92    
93}
94impl TryFrom<char> for Code {
95    type Error = &'static str;
96
97    fn try_from(value: char) -> Result<Self, Self::Error> {
98        match value {
99            '0' => Ok(Code::Color(MCColor::Black)),
100            '1' => Ok(Code::Color(MCColor::DarkBlue)),
101            '2' => Ok(Code::Color(MCColor::DarkGreen)),
102            '3' => Ok(Code::Color(MCColor::DarkAqua)),
103            '4' => Ok(Code::Color(MCColor::DarkRed)),
104            '5' => Ok(Code::Color(MCColor::DarkPurple)),
105            '6' => Ok(Code::Color(MCColor::Gold)),
106            '7' => Ok(Code::Color(MCColor::Gray)),
107            '8' => Ok(Code::Color(MCColor::DarkGray)),
108            '9' => Ok(Code::Color(MCColor::Blue)),
109            'a' => Ok(Code::Color(MCColor::Green)),
110            'b' => Ok(Code::Color(MCColor::Aqua)),
111            'c' => Ok(Code::Color(MCColor::Red)),
112            'd' => Ok(Code::Color(MCColor::LightPurple)),
113            'e' => Ok(Code::Color(MCColor::Yellow)),
114            'f' => Ok(Code::Color(MCColor::White)),
115            'k' => Ok(Code::Effect(Effects::OBFUSCATED)),
116            'l' => Ok(Code::Effect(Effects::BOLD)),
117            'm' => Ok(Code::Effect(Effects::STRIKETHROUGH)),
118            'n' => Ok(Code::Effect(Effects::UNDERLINE)),
119            'o' => Ok(Code::Effect(Effects::ITALIC)),
120            'r' => Ok(Code::Reset),
121            _ => Err("Code not recognized"),
122        }
123    }
124}
125
126bitflags! {
127    pub struct Effects: u8 {
128        const OBFUSCATED = 1 << 0;
129        const BOLD = 1 << 1;
130        const STRIKETHROUGH = 1 << 2;
131        const UNDERLINE = 1 << 3;
132        const ITALIC = 1 << 4;
133    }
134}
135
136#[derive(Debug, PartialEq, Eq)]
137pub struct Span {
138    pub text: String,
139    pub color: Option<MCColor>,
140    pub effects: Effects,
141}
142
143impl Span {
144    pub fn write_out(&self, mut out: impl Write) {
145        let res = execute! {
146            out,
147            SetForegroundColor(self.color.map(|mc_color| mc_color.to_crossterm()).unwrap_or(Color::Reset)),
148            Print(&self.text),
149            SetForegroundColor(Color::Reset),
150        };
151        res.unwrap();
152    }
153
154    pub fn print(&self) {
155        self.write_out(stdout())
156    }
157}
158
159pub fn formatting_tokenize(mut input: &str) -> Vec<Span> {
160    let mut current_color: Option<MCColor> = None;
161    let mut current_effects = Effects::empty();
162    let mut output = Vec::new();
163
164    let mut text_buffer = String::new();
165    while !input.is_empty() {
166        dbg!(&text_buffer);
167        let symbol_index = match input.find('§') {
168            Some(symbol_index) => symbol_index,
169            // No formatting codes left in input
170            None => {
171                text_buffer.push_str(input);
172                output.push(Span {
173                    text: std::mem::take(&mut text_buffer),
174                    color: current_color,
175                    effects: current_effects,
176                });
177                break;
178            }
179        };
180        dbg!(&symbol_index);
181        dbg!(&input[..symbol_index]);
182
183        match
184            // Skip § symbol
185            input.get((symbol_index + 2)..)
186            // Try to parse char as a formatting code
187            .and_then(|code_slice| {
188                // Getting first char from slice
189                code_slice.chars().next().and_then(|first| first.try_into().ok())
190            }) {
191            // Valid code
192            Some(code_type) => {
193                if symbol_index != 0 {
194                    // Use text_buffer incase there is somting left over from previous loops
195                    text_buffer.push_str(&input[..symbol_index]);
196                    output.push( Span { text: std::mem::take(&mut text_buffer), color: current_color, effects: current_effects});
197                }
198                input = &input[(symbol_index + 3)..];
199                match code_type {
200                    Code::Color(c) => current_color = Some(c),
201                    Code::Effect(e) => current_effects |= e,
202                    Code::Reset => {
203                        current_color = None;
204                        current_effects = Effects::empty();
205                    }
206                }
207            },
208            // Invalid code or there is a symbol at the end of input
209            None => {
210                text_buffer.push_str(&input[..(symbol_index+2)]);
211                input = &input[(symbol_index + 2)..];
212            },
213        };
214    }
215    // If there is text left in the buffer it still needs to be emited
216    if !text_buffer.is_empty() {
217        output.push(Span {
218            text: std::mem::take(&mut text_buffer),
219            color: current_color,
220            effects: current_effects,
221        });
222    }
223    output
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    #[test]
230    fn nothing() {
231        assert_eq!(
232            formatting_tokenize("Hello, World!"),
233            vec![Span {
234                text: String::from("Hello, World!"),
235                color: None,
236                effects: Effects::empty()
237            }]
238        );
239    }
240    #[test]
241    fn print_out() {
242        for i in &[
243            Span {
244                text: String::from("Plain"),
245                color: None,
246                effects: Effects::empty(),
247            },
248            Span {
249                text: String::from("then red"),
250                color: Some(MCColor::Red),
251                effects: Effects::empty(),
252            },
253            Span {
254                text: String::from("blue"),
255                color: Some(MCColor::Blue),
256                effects: Effects::empty(),
257            },
258            Span {
259                text: String::from("dark purple"),
260                color: Some(MCColor::DarkPurple),
261                effects: Effects::empty(),
262            },
263            Span {
264                text: String::from("gold"),
265                color: Some(MCColor::Gold),
266                effects: Effects::empty(),
267            },
268        ] {
269            i.print();
270        }
271    }
272    #[test]
273    fn basic() {
274        assert_eq!(
275            formatting_tokenize("Plain§cthen red"),
276            vec![
277                Span {
278                    text: String::from("Plain"),
279                    color: None,
280                    effects: Effects::empty()
281                },
282                Span {
283                    text: String::from("then red"),
284                    color: Some(MCColor::Red),
285                    effects: Effects::empty()
286                }
287            ]
288        );
289    }
290    #[test]
291    fn code_at_start() {
292        assert_eq!(
293            formatting_tokenize("§0Black"),
294            vec![Span {
295                text: String::from("Black"),
296                color: Some(MCColor::Black),
297                effects: Effects::empty()
298            }]
299        );
300    }
301    #[test]
302    fn mutiple_codes() {
303        assert_eq!(
304            formatting_tokenize("Plain§0§lBlack and Bold"),
305            vec![
306                Span {
307                    text: String::from("Plain"),
308                    color: None,
309                    effects: Effects::empty()
310                },
311                Span {
312                    text: String::from("Black and Bold"),
313                    color: Some(MCColor::Black),
314                    effects: Effects::BOLD
315                }
316            ]
317        );
318    }
319    #[test]
320    fn two_effects() {
321        assert_eq!(
322            formatting_tokenize("§n§lUnder Bold"),
323            vec![Span {
324                text: String::from("Under Bold"),
325                color: None,
326                effects: Effects::UNDERLINE | Effects::BOLD
327            }]
328        );
329    }
330    #[test]
331    fn cascading() {
332        assert_eq!(formatting_tokenize("Plain§nUnderlined§eUnder Yellow§oUnder Italic Yellow§9Under Italic Blue§rPlain again"), 
333        vec![Span { text: String::from("Plain"),               color: None,                effects: Effects::empty() },
334             Span { text: String::from("Underlined"),          color: None,                effects: Effects::UNDERLINE },
335             Span { text: String::from("Under Yellow"),        color: Some(MCColor::Yellow), effects: Effects::UNDERLINE },
336             Span { text: String::from("Under Italic Yellow"), color: Some(MCColor::Yellow), effects: Effects::UNDERLINE | Effects::ITALIC },
337             Span { text: String::from("Under Italic Blue"),   color: Some(MCColor::Blue),   effects: Effects::UNDERLINE | Effects::ITALIC },
338             Span { text: String::from("Plain again"),         color: None,                effects: Effects::empty() }]);
339    }
340    #[test]
341    fn ignore_embedded_noncodes() {
342        assert_eq!(
343            formatting_tokenize("I Like §§§ alot!"),
344            vec![Span {
345                text: String::from("I Like §§§ alot!"),
346                color: None,
347                effects: Effects::empty()
348            }]
349        );
350    }
351    #[test]
352    fn trailing_noncodes() {
353        assert_eq!(
354            formatting_tokenize("-> §"),
355            vec![Span {
356                text: String::from("-> §"),
357                color: None,
358                effects: Effects::empty()
359            }]
360        );
361    }
362}