use std::fmt::{self, Display}; use serde::{Serialize, Serializer, ser::SerializeMap}; use crate::{ FormattedText, base_component::BaseComponent, style::{ChatFormatting, Style, TextColor}, }; /// A component that contains text that's the same in all locales. #[derive(Clone, Debug, Default, PartialEq)] pub struct TextComponent { pub base: BaseComponent, pub text: String, } impl Serialize for TextComponent { fn serialize(&self, serializer: S) -> Result where S: Serializer, { if self.base == BaseComponent::default() { return serializer.serialize_str(&self.text); } let mut state = serializer.serialize_map(None)?; state.serialize_entry("text", &self.text)?; self.base.serialize_map::(&mut state)?; state.end() } } #[cfg(feature = "simdnbt")] impl simdnbt::Serialize for TextComponent { fn to_compound(self) -> simdnbt::owned::NbtCompound { let mut compound = simdnbt::owned::NbtCompound::new(); compound.insert("text", self.text); compound.extend(self.base.style.to_compound()); if !self.base.siblings.is_empty() { compound.insert( "extra", simdnbt::owned::NbtList::from( self.base .siblings .into_iter() .map(|component| component.to_compound()) .collect::>(), ), ); } compound } } const LEGACY_FORMATTING_CODE_SYMBOL: char = '§'; /// Convert a legacy color code string into a FormattedText /// 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_text_component(legacy_color_code: &str) -> TextComponent { if legacy_color_code.is_empty() { return TextComponent::new(""); } 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 let mut cur_component = TextComponent::new(""); // 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); let Some(formatting_code) = formatting_code else { i += 1; continue; }; if formatting_code == '#' { let color = legacy_color_code .chars() .skip(i + 1) // 7 to include the # .take(7) .collect::(); if !cur_component.text.is_empty() { // we need to split this into a new component components.push(cur_component.clone()); cur_component.text = "".to_owned(); }; cur_component.base.style.color = TextColor::parse(&color); i += 6; } else if let Some(formatter) = ChatFormatting::from_code(formatting_code) { if !cur_component.text.is_empty() || formatter == ChatFormatting::Reset { // we need to split this into a new component components.push(cur_component.clone()); cur_component.text = "".to_owned(); }; cur_component.base.style.apply_formatting(&formatter); } i += 1; } else { cur_component .text .push(legacy_color_code.chars().nth(i).unwrap()); }; i += 1; } components.push(cur_component); // create the final component by adding all of the components as siblings let mut final_component = TextComponent::new(""); for component in components { final_component.base.siblings.push(component.get()); } final_component } impl TextComponent { pub fn new(text: impl Into) -> Self { let text = text.into(); // if it contains a LEGACY_FORMATTING_CODE_SYMBOL, format it if text.contains(LEGACY_FORMATTING_CODE_SYMBOL) { legacy_color_code_to_text_component(&text) } else { Self { base: BaseComponent::new(), text, } } } fn get(self) -> FormattedText { FormattedText::Text(self) } pub fn with_style(mut self, style: Style) -> Self { *self.base.style = style; self } } impl Display for TextComponent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // this contains the final string will all the ansi escape codes for component in FormattedText::Text(self.clone()).into_iter() { let component_text = match &component { FormattedText::Text(c) => c.text.to_string(), FormattedText::Translatable(c) => c.read()?.to_string(), }; f.write_str(&component_text)?; } Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::style::Ansi; #[test] fn test_hypixel_motd_ansi() { let component = TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_owned()) .get(); assert_eq!( component.to_ansi(), format!( "{GREEN}Hypixel Network {RED}[1.8-1.18]\n{BOLD}{AQUA}HAPPY HOLIDAYS{RESET}", GREEN = Ansi::rgb(ChatFormatting::Green.color().unwrap()), RED = Ansi::rgb(ChatFormatting::Red.color().unwrap()), AQUA = Ansi::rgb(ChatFormatting::Aqua.color().unwrap()), BOLD = Ansi::BOLD, RESET = Ansi::RESET ) ); } #[test] fn test_hypixel_motd_html() { let component = TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_owned()) .get(); assert_eq!( component.to_html(), format!( "{GREEN}Hypixel Network {END_SPAN}{RED}[1.8-1.18]
{END_SPAN}{BOLD_AQUA}HAPPY HOLIDAYS{END_SPAN}", END_SPAN = "", GREEN = "", RED = "", BOLD_AQUA = "", ) ); } #[test] fn test_xss_html() { let component = TextComponent::new("§a&\n§b".to_owned()).get(); assert_eq!( component.to_html(), format!( "{GREEN}<b>&
{END_SPAN}{AQUA}</b>{END_SPAN}", END_SPAN = "
", GREEN = "", AQUA = "", ) ); } #[test] fn test_legacy_color_code_to_component() { let component = TextComponent::new("§lHello §r§1w§2o§3r§4l§5d".to_owned()).get(); assert_eq!( component.to_ansi(), format!( "{BOLD}{WHITE}Hello {RESET}{DARK_BLUE}w{DARK_GREEN}o{DARK_AQUA}r{DARK_RED}l{DARK_PURPLE}d{RESET}", BOLD = Ansi::BOLD, WHITE = Ansi::rgb(ChatFormatting::White.color().unwrap()), RESET = Ansi::RESET, DARK_BLUE = Ansi::rgb(ChatFormatting::DarkBlue.color().unwrap()), DARK_GREEN = Ansi::rgb(ChatFormatting::DarkGreen.color().unwrap()), DARK_AQUA = Ansi::rgb(ChatFormatting::DarkAqua.color().unwrap()), DARK_RED = Ansi::rgb(ChatFormatting::DarkRed.color().unwrap()), DARK_PURPLE = Ansi::rgb(ChatFormatting::DarkPurple.color().unwrap()) ) ); } #[test] fn test_legacy_color_code_with_rgb() { let component = TextComponent::new("§#Ff0000This is a test message".to_owned()).get(); assert_eq!( component.to_ansi(), format!( "{RED}This is a test message{RESET}", RED = Ansi::rgb(0xff0000), RESET = Ansi::RESET ) ); } #[test] fn test_serialize_to_json() { let component = TextComponent::new("Hello §aworld".to_owned()).get(); assert_eq!( serde_json::to_string(&component).unwrap(), "{\"text\":\"\",\"extra\":[\"Hello \",{\"text\":\"world\",\"color\":\"#55FF55\"}]}" ); } }