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 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 input.get((symbol_index + 2)..)
186 .and_then(|code_slice| {
188 code_slice.chars().next().and_then(|first| first.try_into().ok())
190 }) {
191 Some(code_type) => {
193 if symbol_index != 0 {
194 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 None => {
210 text_buffer.push_str(&input[..(symbol_index+2)]);
211 input = &input[(symbol_index + 2)..];
212 },
213 };
214 }
215 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}