From 6026c74430f311c9217b77e7ac07d183efde5bce Mon Sep 17 00:00:00 2001 From: mat Date: Sat, 11 Dec 2021 11:38:12 -0600 Subject: [PATCH] add legacy color codes --- minecraft-chat/src/base_component.rs | 2 +- minecraft-chat/src/component.rs | 76 +++++++++++----------- minecraft-chat/src/style.rs | 80 ++++++++++++++++++++---- minecraft-chat/src/text_component.rs | 77 ++++++++++++++++++++++- minecraft-chat/tests/integration_test.rs | 4 +- 5 files changed, 186 insertions(+), 53 deletions(-) diff --git a/minecraft-chat/src/base_component.rs b/minecraft-chat/src/base_component.rs index 40fb3909..b07e08e7 100644 --- a/minecraft-chat/src/base_component.rs +++ b/minecraft-chat/src/base_component.rs @@ -11,7 +11,7 @@ impl BaseComponent { pub fn new() -> Self { Self { siblings: Vec::new(), - style: Style::new(), + style: Style::default(), } } } diff --git a/minecraft-chat/src/component.rs b/minecraft-chat/src/component.rs index c59a5d5d..300a1228 100644 --- a/minecraft-chat/src/component.rs +++ b/minecraft-chat/src/component.rs @@ -1,5 +1,3 @@ - - use serde_json; use crate::{ @@ -11,8 +9,8 @@ use crate::{ #[derive(Clone, Debug)] pub enum Component { - TextComponent(TextComponent), - TranslatableComponent(TranslatableComponent), + Text(TextComponent), + Translatable(TranslatableComponent), } /// A chat component @@ -23,7 +21,7 @@ impl Component { // if it's primitive, make it a text component if !json.is_array() && !json.is_object() { - return Ok(Component::TextComponent(TextComponent::new( + return Ok(Component::Text(TextComponent::new( json.as_str().unwrap_or("").to_string(), ))); } @@ -31,7 +29,7 @@ impl Component { else if json.is_object() { if json.get("text").is_some() { let text = json.get("text").unwrap().as_str().unwrap_or("").to_string(); - component = Component::TextComponent(TextComponent::new(text)); + component = Component::Text(TextComponent::new(text)); } else if json.get("translate").is_some() { let translate = json.get("translate").unwrap().to_string(); if json.get("with").is_some() { @@ -41,7 +39,7 @@ impl Component { // if it's a string component with no styling and no siblings, just add a string to with_array // otherwise add the component to the array let c = Component::new(&with[i])?; - if let Component::TextComponent(text_component) = c { + if let Component::Text(text_component) = c { if text_component.base.siblings.is_empty() && text_component.base.style.is_empty() { @@ -51,15 +49,12 @@ impl Component { } with_array.push(StringOrComponent::Component(Component::new(&with[i])?)); } - component = Component::TranslatableComponent(TranslatableComponent::new( - translate, with_array, - )); + component = + Component::Translatable(TranslatableComponent::new(translate, with_array)); } else { // if it doesn't have a "with", just have the with_array be empty - component = Component::TranslatableComponent(TranslatableComponent::new( - translate, - Vec::new(), - )); + component = + Component::Translatable(TranslatableComponent::new(translate, Vec::new())); } } else if json.get("score").is_some() { // object = GsonHelper.getAsJsonObject(jsonObject, "score"); @@ -157,15 +152,15 @@ impl Component { pub fn get_base_mut(&mut self) -> &mut BaseComponent { match self { - Self::TextComponent(c) => &mut c.base, - Self::TranslatableComponent(c) => &mut c.base, + Self::Text(c) => &mut c.base, + Self::Translatable(c) => &mut c.base, } } pub fn get_base(&self) -> &BaseComponent { match self { - Self::TextComponent(c) => &c.base, - Self::TranslatableComponent(c) => &c.base, + Self::Text(c) => &c.base, + Self::Translatable(c) => &c.base, } } @@ -182,30 +177,17 @@ impl Component { Ok(None) } - /// Recursively call the function for every component in this component - pub fn visit(&self, f: &mut F) - where - // The closure takes an `i32` and returns an `i32`. - F: FnMut(&Component), - { - f(self); - self.get_base() - .siblings - .iter() - .for_each(|s| Component::visit(s, f)); - } - /// Convert this component into an ansi string pub fn to_ansi(&self) -> String { // this contains the final string will all the ansi escape codes let mut built_string = String::new(); // this style will update as we visit components - let mut running_style = Style::new(); + let mut running_style = Style::default(); - self.visit(&mut |component| { - let component_text = match component { - Self::TextComponent(c) => &c.text, - Self::TranslatableComponent(c) => &c.key, + for component in self.clone().into_iter() { + let component_text = match &component { + Self::Text(c) => &c.text, + Self::Translatable(c) => &c.key, }; let component_style = &component.get_base().style; @@ -214,12 +196,30 @@ impl Component { built_string.push_str(component_text); running_style.apply(component_style); - }); + } if !running_style.is_empty() { - built_string.push_str("\x1b[m"); + built_string.push_str("\u{1b}[m"); } built_string } } + +impl IntoIterator for Component { + /// Recursively call the function for every component in this component + fn into_iter(self) -> Self::IntoIter { + let base = self.get_base(); + let siblings = base.siblings.clone(); + let mut v: Vec = Vec::with_capacity(siblings.len() + 1); + v.push(self); + for sibling in siblings { + v.extend(sibling.into_iter()); + } + + v.into_iter() + } + + type Item = Component; + type IntoIter = std::vec::IntoIter; +} diff --git a/minecraft-chat/src/style.rs b/minecraft-chat/src/style.rs index ecde7c9e..14fabec5 100644 --- a/minecraft-chat/src/style.rs +++ b/minecraft-chat/src/style.rs @@ -66,16 +66,16 @@ pub struct ChatFormatting<'a> { pub struct Ansi {} impl Ansi { - pub const BOLD: &'static str = "\x1b[1m"; - pub const ITALIC: &'static str = "\x1b[3m"; - pub const UNDERLINED: &'static str = "\x1b[4m"; - pub const STRIKETHROUGH: &'static str = "\x1b[9m"; - pub const OBFUSCATED: &'static str = "\x1b[8m"; - pub const RESET: &'static str = "\x1b[m"; + pub const BOLD: &'static str = "\u{1b}[1m"; + pub const ITALIC: &'static str = "\u{1b}[3m"; + pub const UNDERLINED: &'static str = "\u{1b}[4m"; + pub const STRIKETHROUGH: &'static str = "\u{1b}[9m"; + pub const OBFUSCATED: &'static str = "\u{1b}[8m"; + pub const RESET: &'static str = "\u{1b}[m"; pub fn rgb(value: u32) -> String { format!( - "\x1b[38;2;{};{};{}m", + "\u{1b}[38;2;{};{};{}m", (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF @@ -159,6 +159,15 @@ impl<'a> ChatFormatting<'a> { color, } } + + pub fn from_code(code: char) -> Result<&'static ChatFormatting<'static>, String> { + for formatter in &ChatFormatting::FORMATTERS { + if formatter.code == code { + return Ok(formatter); + } + } + Err(format!("Invalid formatting code {}", code)) + } } impl TextColor { @@ -212,7 +221,7 @@ pub struct Style { } impl Style { - pub fn new() -> Style { + pub fn default() -> Style { Style { color: None, bold: None, @@ -244,7 +253,7 @@ impl Style { obfuscated, } } else { - Style::new() + Style::default() }; } @@ -275,7 +284,9 @@ impl Style { } else if self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true) { true // if it used to be obfuscated and now it's not, reset - } else { self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true) } + } else { + self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true) + } }; let mut ansi_codes = String::new(); @@ -287,7 +298,7 @@ impl Style { ansi_codes.push_str(Ansi::RESET); let mut updated_after = self.clone(); updated_after.apply(after); - (Style::new(), updated_after) + (Style::default(), updated_after) } else { (self.clone(), after.clone()) }; @@ -355,6 +366,37 @@ impl Style { self.obfuscated = Some(*obfuscated); } } + + /// Apply a ChatFormatting to this style + pub fn apply_formatting(&mut self, formatting: &ChatFormatting) { + match formatting { + &ChatFormatting::BOLD => self.bold = Some(true), + &ChatFormatting::ITALIC => self.italic = Some(true), + &ChatFormatting::UNDERLINE => self.underlined = Some(true), + &ChatFormatting::STRIKETHROUGH => self.strikethrough = Some(true), + &ChatFormatting::OBFUSCATED => self.obfuscated = Some(true), + &ChatFormatting::RESET => { + self.color = None; + self.bold = None; + self.italic = None; + self.underlined = None; + self.strikethrough = None; + self.obfuscated = None; + } + &ChatFormatting { + name: _, + code: _, + is_format: _, + id: _, + color, + } => { + // if it's a color, set it + if let Some(color) = color { + self.color = Some(TextColor::from_rgb(color)); + } + } + } + } } #[cfg(test)] @@ -422,4 +464,20 @@ mod tests { let ansi_difference = style_a.compare_ansi(&style_b); assert_eq!(ansi_difference, Ansi::ITALIC) } + + #[test] + fn test_from_code() { + assert_eq!( + ChatFormatting::from_code('a').unwrap(), + &ChatFormatting::GREEN + ); + } + + #[test] + fn test_apply_formatting() { + let mut style = Style::default(); + style.apply_formatting(&ChatFormatting::BOLD); + style.apply_formatting(&ChatFormatting::RED); + assert_eq!(style.color, Some(TextColor::from_rgb(16733525))); + } } diff --git a/minecraft-chat/src/text_component.rs b/minecraft-chat/src/text_component.rs index 9bca1fa0..66bde690 100644 --- a/minecraft-chat/src/text_component.rs +++ b/minecraft-chat/src/text_component.rs @@ -1,4 +1,4 @@ -use crate::base_component::BaseComponent; +use crate::{base_component::BaseComponent, component::Component, style::ChatFormatting}; #[derive(Clone, Debug)] pub struct TextComponent { @@ -6,6 +6,63 @@ pub struct TextComponent { pub text: String, } +const LEGACY_FORMATTING_CODE_SYMBOL: char = '§'; + +/// Convert a legacy color code string into a Component +/// Technically in Minecraft this is done when displaying the text, but AFAIK it's the same as just doing it in TextComponent +pub fn legacy_color_code_to_component(legacy_color_code: &str) -> Component { + let mut components: Vec = Vec::with_capacity(1); + // iterate over legacy_color_code, if it starts with LEGACY_COLOR_CODE_SYMBOL then read the next character and get the style from that + // otherwise, add the character to the text + + // we don't use a normal for loop since we need to be able to skip after reading the formatter code symbol + let mut i = 0; + while i < legacy_color_code.chars().count() { + if legacy_color_code.chars().nth(i).unwrap() == LEGACY_FORMATTING_CODE_SYMBOL { + let formatting_code = legacy_color_code.chars().nth(i + 1).unwrap(); + if let Ok(formatter) = ChatFormatting::from_code(formatting_code) { + if components.is_empty() || components.last().unwrap().text.is_empty() { + components.push(TextComponent::new("".to_string())); + } + println!( + "applying formatter {:?} {:?}", + components.last_mut().unwrap().base.style, + formatter + ); + components + .last_mut() + .unwrap() + .base + .style + .apply_formatting(formatter); + println!( + "applied formatter {:?}", + components.last_mut().unwrap().base.style + ); + } + i += 1; + } else { + if components.is_empty() { + components.push(TextComponent::new("".to_string())); + } + components + .last_mut() + .unwrap() + .text + .push(legacy_color_code.chars().nth(i).unwrap()); + }; + i += 1; + } + + // create the final component by using the first one as the base, and then adding the rest as siblings + let mut final_component = components.remove(0); + for component in components { + final_component.base.siblings.push(component.get()); + } + + final_component.get() +} + impl<'a> TextComponent { pub fn new(text: String) -> Self { Self { @@ -17,4 +74,22 @@ impl<'a> TextComponent { pub fn to_string(&self) -> String { self.text.clone() } + + fn get(self) -> Component { + Component::Text(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_legacy_color_code_to_component() { + let component = legacy_color_code_to_component("§lHello §r§1w§2o§3r§4l§5d"); + assert_eq!( + component.to_ansi(), + "\u{1b}[38;2;170;0;170mHello world\u{1b}[m" + ); + } } diff --git a/minecraft-chat/tests/integration_test.rs b/minecraft-chat/tests/integration_test.rs index 1a010a13..3feff1ed 100644 --- a/minecraft-chat/tests/integration_test.rs +++ b/minecraft-chat/tests/integration_test.rs @@ -2,7 +2,7 @@ use minecraft_chat::{ component::Component, style::{Ansi, ChatFormatting, TextColor}, }; -use serde_json::{Value}; +use serde_json::Value; #[test] fn basic_ansi_test() { @@ -17,7 +17,7 @@ fn basic_ansi_test() { let component = Component::new(&j).unwrap(); assert_eq!( component.to_ansi(), - "\x1b[1m\x1b[38;2;255;85;85mhello\x1b[m" + "\u{1b}[1m\u{1b}[38;2;255;85;85mhello\u{1b}[m" ); }