diff options
| author | mat <github@matdoes.dev> | 2021-12-15 23:10:55 -0600 |
|---|---|---|
| committer | mat <github@matdoes.dev> | 2021-12-15 23:10:55 -0600 |
| commit | 9642558f8f8d983a7087f15d68be8cf07a85f0c2 (patch) | |
| tree | 5f0a967f005cd5db510a13ab290c8ad6669b25aa /minecraft-chat | |
| parent | 72aefe871ca4983431b1a0b707b472e73ffea836 (diff) | |
| download | azalea-drasl-9642558f8f8d983a7087f15d68be8cf07a85f0c2.tar.xz | |
azalea
Diffstat (limited to 'minecraft-chat')
| -rw-r--r-- | minecraft-chat/Cargo.toml | 11 | ||||
| -rw-r--r-- | minecraft-chat/src/base_component.rs | 23 | ||||
| -rw-r--r-- | minecraft-chat/src/component.rs | 262 | ||||
| -rw-r--r-- | minecraft-chat/src/events.rs | 26 | ||||
| -rw-r--r-- | minecraft-chat/src/lib.rs | 11 | ||||
| -rw-r--r-- | minecraft-chat/src/style.rs | 478 | ||||
| -rw-r--r-- | minecraft-chat/src/text_component.rs | 122 | ||||
| -rw-r--r-- | minecraft-chat/src/translatable_component.rs | 24 | ||||
| -rw-r--r-- | minecraft-chat/tests/integration_test.rs | 75 |
9 files changed, 0 insertions, 1032 deletions
diff --git a/minecraft-chat/Cargo.toml b/minecraft-chat/Cargo.toml deleted file mode 100644 index aa803864..00000000 --- a/minecraft-chat/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -edition = "2021" -name = "minecraft-chat" -version = "0.1.0" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -lazy_static = "1.4.0" -serde = "^1.0.130" -serde_json = "^1.0.72" diff --git a/minecraft-chat/src/base_component.rs b/minecraft-chat/src/base_component.rs deleted file mode 100644 index fa39a11c..00000000 --- a/minecraft-chat/src/base_component.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{component::Component, style::Style}; - -#[derive(Clone, Debug)] -pub struct BaseComponent { - // implements mutablecomponent - pub siblings: Vec<Component>, - pub style: Style, -} - -impl BaseComponent { - pub fn new() -> Self { - Self { - siblings: Vec::new(), - style: Style::default(), - } - } -} - -impl Default for BaseComponent { - fn default() -> Self { - Self::new() - } -} diff --git a/minecraft-chat/src/component.rs b/minecraft-chat/src/component.rs deleted file mode 100644 index 1ed1f836..00000000 --- a/minecraft-chat/src/component.rs +++ /dev/null @@ -1,262 +0,0 @@ -use serde::{de, Deserialize, Deserializer}; - -use crate::{ - base_component::BaseComponent, - style::{ChatFormatting, Style}, - text_component::TextComponent, - translatable_component::{StringOrComponent, TranslatableComponent}, -}; - -#[derive(Clone, Debug)] -pub enum Component { - Text(TextComponent), - Translatable(TranslatableComponent), -} - -lazy_static! { - pub static ref DEFAULT_STYLE: Style = Style { - color: Some(ChatFormatting::WHITE.try_into().unwrap()), - ..Style::default() - }; -} - -/// A chat component -impl Component { - // TODO: is it possible to use a macro so this doesn't have to be duplicated? - - pub fn get_base_mut(&mut self) -> &mut BaseComponent { - match self { - Self::Text(c) => &mut c.base, - Self::Translatable(c) => &mut c.base, - } - } - - pub fn get_base(&self) -> &BaseComponent { - match self { - Self::Text(c) => &c.base, - Self::Translatable(c) => &c.base, - } - } - - /// Add a component as a sibling of this one - fn append(&mut self, sibling: Component) { - self.get_base_mut().siblings.push(sibling); - } - - /// Get the "separator" component from the json - fn parse_separator(json: &serde_json::Value) -> Result<Option<Component>, serde_json::Error> { - if json.get("separator").is_some() { - return Ok(Some(Component::deserialize( - json.get("separator").unwrap(), - )?)); - } - Ok(None) - } - - /// Convert this component into an ansi string - pub fn to_ansi(&self, default_style: Option<&Style>) -> String { - // default the default_style to white if it's not set - let default_style: &Style = default_style.unwrap_or(&DEFAULT_STYLE); - - // 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::default(); - - 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; - - let ansi_text = running_style.compare_ansi(component_style, default_style); - built_string.push_str(&ansi_text); - built_string.push_str(component_text); - - running_style.apply(component_style); - } - - if !running_style.is_empty() { - 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<Component> = 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<Self::Item>; -} - -impl<'de> Deserialize<'de> for Component { - fn deserialize<D>(de: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - let json: serde_json::Value = serde::Deserialize::deserialize(de)?; - - // we create a component that we might add siblings to - let mut component: Component; - - // if it's primitive, make it a text component - if !json.is_array() && !json.is_object() { - return Ok(Component::Text(TextComponent::new( - json.as_str().unwrap_or("").to_string(), - ))); - } - // if it's an object, do things with { text } and stuff - 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::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() { - let with = json.get("with").unwrap().as_array().unwrap(); - let mut with_array = Vec::with_capacity(with.len()); - for item in with { - // 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::deserialize(item).map_err(de::Error::custom)?; - if let Component::Text(text_component) = c { - if text_component.base.siblings.is_empty() - && text_component.base.style.is_empty() - { - with_array.push(StringOrComponent::String(text_component.text)); - break; - } - } - with_array.push(StringOrComponent::Component( - Component::deserialize(item).map_err(de::Error::custom)?, - )); - } - 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::Translatable(TranslatableComponent::new(translate, Vec::new())); - } - } else if json.get("score").is_some() { - // object = GsonHelper.getAsJsonObject(jsonObject, "score"); - let score_json = json.get("score").unwrap(); - // if (!object.has("name") || !object.has("objective")) throw new JsonParseException("A score component needs a least a name and an objective"); - // ScoreComponent scoreComponent = new ScoreComponent(GsonHelper.getAsString((JsonObject)object, "name"), GsonHelper.getAsString((JsonObject)object, "objective")); - if score_json.get("name").is_none() || score_json.get("objective").is_none() { - return Err(de::Error::missing_field( - "A score component needs at least a name and an objective", - )); - } - // TODO - return Err(de::Error::custom( - "score text components aren't yet supported", - )); - // component = ScoreComponent - } else if json.get("selector").is_some() { - // } else if (jsonObject.has("selector")) { - // object = this.parseSeparator(type, jsonDeserializationContext, jsonObject); - // SelectorComponent selectorComponent = new SelectorComponent(GsonHelper.getAsString(jsonObject, "selector"), (Optional<Component>)object); - - return Err(de::Error::custom( - "selector text components aren't yet supported", - )); - // } else if (jsonObject.has("keybind")) { - // KeybindComponent keybindComponent = new KeybindComponent(GsonHelper.getAsString(jsonObject, "keybind")); - } else if json.get("keybind").is_some() { - return Err(de::Error::custom( - "keybind text components aren't yet supported", - )); - } else { - // } else { - // if (!jsonObject.has("nbt")) throw new JsonParseException("Don't know how to turn " + jsonElement + " into a Component"); - if json.get("nbt").is_none() { - return Err(de::Error::custom( - format!("Don't know how to turn {} into a Component", json).as_str(), - )); - } - // object = GsonHelper.getAsString(jsonObject, "nbt"); - let _nbt = json.get("nbt").unwrap().to_string(); - // Optional<Component> optional = this.parseSeparator(type, jsonDeserializationContext, jsonObject); - let _separator = Component::parse_separator(&json).map_err(de::Error::custom)?; - - let _interpret = match json.get("interpret") { - Some(v) => v.as_bool().ok_or(Some(false)).unwrap(), - None => false, - }; - // boolean bl = GsonHelper.getAsBoolean(jsonObject, "interpret", false); - // if (jsonObject.has("block")) { - if json.get("block").is_some() {} - return Err(de::Error::custom( - "nbt text components aren't yet supported", - )); - // NbtComponent.BlockNbtComponent blockNbtComponent = new NbtComponent.BlockNbtComponent((String)object, bl, GsonHelper.getAsString(jsonObject, "block"), optional); - // } else if (jsonObject.has("entity")) { - // NbtComponent.EntityNbtComponent entityNbtComponent = new NbtComponent.EntityNbtComponent((String)object, bl, GsonHelper.getAsString(jsonObject, "entity"), optional); - // } else { - // if (!jsonObject.has("storage")) throw new JsonParseException("Don't know how to turn " + jsonElement + " into a Component"); - // NbtComponent.StorageNbtComponent storageNbtComponent = new NbtComponent.StorageNbtComponent((String)object, bl, new ResourceLocation(GsonHelper.getAsString(jsonObject, "storage")), optional); - // } - // } - } - // if (jsonObject.has("extra")) { - // object = GsonHelper.getAsJsonArray(jsonObject, "extra"); - // if (object.size() <= 0) throw new JsonParseException("Unexpected empty array of components"); - // for (int i = 0; i < object.size(); ++i) { - // var5_17.append(this.deserialize(object.get(i), type, jsonDeserializationContext)); - // } - // } - // var5_17.setStyle((Style)jsonDeserializationContext.deserialize(jsonElement, Style.class)); - // return var5_17; - // } - if json.get("extra").is_some() { - let extra = match json.get("extra").unwrap().as_array() { - Some(r) => r, - None => return Err(de::Error::custom("Extra isn't an array")), - }; - if extra.is_empty() { - return Err(de::Error::custom("Unexpected empty array of components")); - } - for extra_component in extra { - let sibling = - Component::deserialize(extra_component).map_err(de::Error::custom)?; - component.append(sibling); - } - } - - let style = Style::deserialize(&json); - component.get_base_mut().style = style; - - return Ok(component); - } - // ok so it's not an object, if it's an array deserialize every item - else if !json.is_array() { - return Err(de::Error::custom( - format!("Don't know how to turn {} into a Component", json).as_str(), - )); - } - let json_array = json.as_array().unwrap(); - // the first item in the array is the one that we're gonna return, the others are siblings - let mut component = Component::deserialize(&json_array[0]).map_err(de::Error::custom)?; - for i in 1..json_array.len() { - component.append( - Component::deserialize(json_array.get(i).unwrap()).map_err(de::Error::custom)?, - ); - } - Ok(component) - } -} diff --git a/minecraft-chat/src/events.rs b/minecraft-chat/src/events.rs deleted file mode 100644 index a547169e..00000000 --- a/minecraft-chat/src/events.rs +++ /dev/null @@ -1,26 +0,0 @@ -enum ClickAction { - OPEN_URL = Action::new("open_url", true), - OPEN_FILE = Action::new("open_file", false), - RUN_COMMAND = Action::new("run_command", true), - SUGGEST_COMMAND = Action::new("suggest_command", true), - CHANGE_PAGE = Action::new("change_page", true), - COPY_TO_CLIPBOARD = Action::new("copy_to_clipboard", true), -} - -struct ClickAction { - pub name: String, - pub allow_from_server: bool, -} - -impl ClickAction { - fn new(name: &str, allow_from_server: bool) -> Self { - Self { - name: name.to_string(), - allow_from_server, - } - } -} - -struct ClickEvent { - action: ClickAction, -} diff --git a/minecraft-chat/src/lib.rs b/minecraft-chat/src/lib.rs deleted file mode 100644 index b7035e13..00000000 --- a/minecraft-chat/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Things for working with Minecraft chat messages. -//! This was inspired by Minecraft and prismarine-chat. - -#[macro_use] -extern crate lazy_static; - -pub mod base_component; -pub mod component; -pub mod style; -pub mod text_component; -pub mod translatable_component; diff --git a/minecraft-chat/src/style.rs b/minecraft-chat/src/style.rs deleted file mode 100644 index 4e3b24de..00000000 --- a/minecraft-chat/src/style.rs +++ /dev/null @@ -1,478 +0,0 @@ -use std::{collections::HashMap, fmt}; - -use serde_json::Value; - -#[derive(Clone, PartialEq, Debug)] -pub struct TextColor { - pub value: u32, - pub name: Option<String>, -} - -impl TextColor { - pub fn parse(value: String) -> Result<TextColor, String> { - if value.starts_with('#') { - let n = value.chars().skip(1).collect::<String>(); - let n = u32::from_str_radix(&n, 16).unwrap(); - return Ok(TextColor::from_rgb(n)); - } - let color_option = NAMED_COLORS.get(&value.to_ascii_uppercase()); - if let Some(color) = color_option { - return Ok(color.clone()); - } - Err(format!("Invalid color {}", value)) - } - - fn from_rgb(value: u32) -> TextColor { - TextColor { value, name: None } - } -} - -lazy_static! { - static ref LEGACY_FORMAT_TO_COLOR: HashMap<&'static ChatFormatting<'static>, TextColor> = { - let mut legacy_format_to_color = HashMap::new(); - for formatter in &ChatFormatting::FORMATTERS { - if !formatter.is_format && *formatter != ChatFormatting::RESET { - legacy_format_to_color.insert( - formatter, - TextColor { - value: formatter.color.unwrap(), - name: Some(formatter.name.to_string()), - }, - ); - } - } - legacy_format_to_color - }; - static ref NAMED_COLORS: HashMap<String, TextColor> = { - let mut named_colors = HashMap::new(); - for color in LEGACY_FORMAT_TO_COLOR.values() { - named_colors.insert(color.name.clone().unwrap(), color.clone()); - } - named_colors - }; -} - -#[derive(Clone, PartialEq, Eq, Hash, Debug)] -pub struct ChatFormatting<'a> { - pub name: &'a str, - pub code: char, - pub is_format: bool, - pub id: i32, - pub color: Option<u32>, -} - -pub struct Ansi {} -impl Ansi { - 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!( - "\u{1b}[38;2;{};{};{}m", - (value >> 16) & 0xFF, - (value >> 8) & 0xFF, - value & 0xFF - ) - } -} - -impl<'a> ChatFormatting<'a> { - pub const BLACK: ChatFormatting<'a> = ChatFormatting::new("BLACK", '0', false, 0, Some(0)); - pub const DARK_BLUE: ChatFormatting<'a> = - ChatFormatting::new("DARK_BLUE", '1', false, 1, Some(170)); - pub const DARK_GREEN: ChatFormatting<'a> = - ChatFormatting::new("DARK_GREEN", '2', false, 2, Some(43520)); - pub const DARK_AQUA: ChatFormatting<'a> = - ChatFormatting::new("DARK_AQUA", '3', false, 3, Some(43690)); - pub const DARK_RED: ChatFormatting<'a> = - ChatFormatting::new("DARK_RED", '4', false, 4, Some(1114112)); - pub const DARK_PURPLE: ChatFormatting<'a> = - ChatFormatting::new("DARK_PURPLE", '5', false, 5, Some(11141290)); - pub const GOLD: ChatFormatting<'a> = ChatFormatting::new("GOLD", '6', false, 6, Some(16755200)); - pub const GRAY: ChatFormatting<'a> = ChatFormatting::new("GRAY", '7', false, 7, Some(11184810)); - pub const DARK_GRAY: ChatFormatting<'a> = - ChatFormatting::new("DARK_GRAY", '8', false, 8, Some(5592405)); - pub const BLUE: ChatFormatting<'a> = ChatFormatting::new("BLUE", '9', false, 9, Some(5592575)); - pub const GREEN: ChatFormatting<'a> = - ChatFormatting::new("GREEN", 'a', false, 10, Some(5635925)); - pub const AQUA: ChatFormatting<'a> = ChatFormatting::new("AQUA", 'b', false, 11, Some(5636095)); - pub const RED: ChatFormatting<'a> = ChatFormatting::new("RED", 'c', false, 12, Some(16733525)); - pub const LIGHT_PURPLE: ChatFormatting<'a> = - ChatFormatting::new("LIGHT_PURPLE", 'd', false, 13, Some(16733695)); - pub const YELLOW: ChatFormatting<'a> = - ChatFormatting::new("YELLOW", 'e', false, 14, Some(16777045)); - pub const WHITE: ChatFormatting<'a> = - ChatFormatting::new("WHITE", 'f', false, 15, Some(16777215)); - pub const OBFUSCATED: ChatFormatting<'a> = - ChatFormatting::new("OBFUSCATED", 'k', true, -1, None); - pub const STRIKETHROUGH: ChatFormatting<'a> = - ChatFormatting::new("STRIKETHROUGH", 'm', true, -1, None); - pub const BOLD: ChatFormatting<'a> = ChatFormatting::new("BOLD", 'l', true, -1, None); - pub const UNDERLINE: ChatFormatting<'a> = ChatFormatting::new("UNDERLINE", 'n', true, -1, None); - pub const ITALIC: ChatFormatting<'a> = ChatFormatting::new("ITALIC", 'o', true, -1, None); - pub const RESET: ChatFormatting<'a> = ChatFormatting::new("RESET", 'r', true, -1, None); - - pub const FORMATTERS: [ChatFormatting<'a>; 22] = [ - ChatFormatting::BLACK, - ChatFormatting::DARK_BLUE, - ChatFormatting::DARK_GREEN, - ChatFormatting::DARK_AQUA, - ChatFormatting::DARK_RED, - ChatFormatting::DARK_PURPLE, - ChatFormatting::GOLD, - ChatFormatting::GRAY, - ChatFormatting::DARK_GRAY, - ChatFormatting::BLUE, - ChatFormatting::GREEN, - ChatFormatting::AQUA, - ChatFormatting::RED, - ChatFormatting::LIGHT_PURPLE, - ChatFormatting::YELLOW, - ChatFormatting::WHITE, - ChatFormatting::OBFUSCATED, - ChatFormatting::STRIKETHROUGH, - ChatFormatting::BOLD, - ChatFormatting::UNDERLINE, - ChatFormatting::ITALIC, - ChatFormatting::RESET, - ]; - - const fn new( - name: &str, - code: char, - is_format: bool, - id: i32, - color: Option<u32>, - ) -> ChatFormatting { - ChatFormatting { - name, - code, - is_format, - id, - 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 { - fn new(value: u32, name: Option<String>) -> Self { - Self { value, name } - } - - pub fn format(&self) -> String { - format!("#{:06X}", self.value) - } -} - -impl fmt::Display for TextColor { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(name) = &self.name { - write!(f, "{}", name.clone()) - } else { - write!(f, "{}", self.format()) - } - } -} - -// from ChatFormatting to TextColor -impl TryFrom<ChatFormatting<'_>> for TextColor { - type Error = String; - - fn try_from(formatter: ChatFormatting<'_>) -> Result<Self, Self::Error> { - if formatter.is_format { - return Err(format!("{} is not a color", formatter.name)); - } - let color = formatter.color.unwrap_or(0); - Ok(Self::new(color, Some(formatter.name.to_string()))) - } -} - -#[derive(Clone, Debug)] -pub struct Style { - // these are options instead of just bools because None is different than false in this case - pub color: Option<TextColor>, - pub bold: Option<bool>, - pub italic: Option<bool>, - pub underlined: Option<bool>, - pub strikethrough: Option<bool>, - pub obfuscated: Option<bool>, - /// Whether it should reset the formatting before applying these styles - pub reset: bool, -} - -impl Style { - pub fn default() -> Self { - Self::empty() - } - - pub fn empty() -> Self { - Self { - color: None, - bold: None, - italic: None, - underlined: None, - strikethrough: None, - obfuscated: None, - reset: false, - } - } - - pub fn deserialize(json: &Value) -> Style { - return if json.is_object() { - let json_object = json.as_object().unwrap(); - let bold = json_object.get("bold").and_then(|v| v.as_bool()); - let italic = json_object.get("italic").and_then(|v| v.as_bool()); - let underlined = json_object.get("underlined").and_then(|v| v.as_bool()); - let strikethrough = json_object.get("strikethrough").and_then(|v| v.as_bool()); - let obfuscated = json_object.get("obfuscated").and_then(|v| v.as_bool()); - let color: Option<TextColor> = json_object - .get("color") - .and_then(|v| v.as_str()) - .and_then(|v| TextColor::parse(v.to_string()).ok()); - Style { - color, - bold, - italic, - underlined, - strikethrough, - obfuscated, - ..Style::default() - } - } else { - Style::default() - }; - } - - /// Check if a style has no attributes set - pub fn is_empty(&self) -> bool { - self.color.is_none() - && self.bold.is_none() - && self.italic.is_none() - && self.underlined.is_none() - && self.strikethrough.is_none() - && self.obfuscated.is_none() - } - - /// find the necessary ansi code to get from this style to another - pub fn compare_ansi(&self, after: &Style, default_style: &Style) -> String { - let should_reset = after.reset || - // if it used to be bold and now it's not, reset - (self.bold.unwrap_or(false) && !after.bold.unwrap_or(true)) || - // if it used to be italic and now it's not, reset - (self.italic.unwrap_or(false) && !after.italic.unwrap_or(true)) || - // if it used to be underlined and now it's not, reset - (self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true)) || - // if it used to be strikethrough and now it's not, reset - (self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true)) || - // if it used to be obfuscated and now it's not, reset - (self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true)); - - let mut ansi_codes = String::new(); - - let empty_style = Style::empty(); - - let (before, after) = if should_reset { - ansi_codes.push_str(Ansi::RESET); - let mut updated_after = if after.reset { - default_style.clone() - } else { - self.clone() - }; - updated_after.apply(after); - (&empty_style, updated_after) - } else { - (self, after.clone()) - }; - - // if bold used to be false/default and now it's true, set bold - if !before.bold.unwrap_or(false) && after.bold.unwrap_or(false) { - ansi_codes.push_str(Ansi::BOLD); - } - // if italic used to be false/default and now it's true, set italic - if !before.italic.unwrap_or(false) && after.italic.unwrap_or(false) { - ansi_codes.push_str(Ansi::ITALIC); - } - // if underlined used to be false/default and now it's true, set underlined - if !before.underlined.unwrap_or(false) && after.underlined.unwrap_or(false) { - ansi_codes.push_str(Ansi::UNDERLINED); - } - // if strikethrough used to be false/default and now it's true, set strikethrough - if !before.strikethrough.unwrap_or(false) && after.strikethrough.unwrap_or(false) { - ansi_codes.push_str(Ansi::STRIKETHROUGH); - } - // if obfuscated used to be false/default and now it's true, set obfuscated - if !before.obfuscated.unwrap_or(false) && after.obfuscated.unwrap_or(false) { - ansi_codes.push_str(Ansi::OBFUSCATED); - } - - // if the new color is different and not none, set color - let color_changed = { - if before.color.is_none() && after.color.is_some() { - true - } else if before.color.is_some() && after.color.is_some() { - before.color.clone().unwrap().value != after.color.as_ref().unwrap().value - } else { - false - } - }; - - if color_changed { - let after_color = after.color.as_ref().unwrap(); - ansi_codes.push_str(&Ansi::rgb(after_color.value)); - } - - ansi_codes - } - - /// Apply another style to this one - pub fn apply(&mut self, style: &Style) { - if let Some(color) = &style.color { - self.color = Some(color.clone()); - } - if let Some(bold) = &style.bold { - self.bold = Some(*bold); - } - if let Some(italic) = &style.italic { - self.italic = Some(*italic); - } - if let Some(underlined) = &style.underlined { - self.underlined = Some(*underlined); - } - if let Some(strikethrough) = &style.strikethrough { - self.strikethrough = Some(*strikethrough); - } - if let Some(obfuscated) = &style.obfuscated { - 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.reset = true, - 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)] -mod tests { - use crate::component::DEFAULT_STYLE; - - use super::*; - - #[test] - fn text_color_named_colors() { - assert_eq!(TextColor::parse("red".to_string()).unwrap().value, 16733525); - } - #[test] - fn text_color_hex_colors() { - assert_eq!( - TextColor::parse("#a1b2c3".to_string()).unwrap().value, - 10597059 - ); - } - - #[test] - fn ansi_difference_should_reset() { - let style_a = Style { - bold: Some(true), - italic: Some(true), - ..Style::default() - }; - let style_b = Style { - bold: Some(false), - ..Style::default() - }; - let ansi_difference = style_a.compare_ansi(&style_b, &Style::default()); - assert_eq!( - ansi_difference, - format!( - "{reset}{italic}", - reset = Ansi::RESET, - italic = Ansi::ITALIC - ) - ) - } - #[test] - fn ansi_difference_shouldnt_reset() { - let style_a = Style { - bold: Some(true), - ..Style::default() - }; - let style_b = Style { - italic: Some(true), - ..Style::default() - }; - let ansi_difference = style_a.compare_ansi(&style_b, &Style::default()); - assert_eq!(ansi_difference, Ansi::ITALIC) - } - - #[test] - fn ansi_difference_explicit_reset() { - let style_a = Style { - bold: Some(true), - ..Style::empty() - }; - let style_b = Style { - italic: Some(true), - reset: true, - ..Style::empty() - }; - let ansi_difference = style_a.compare_ansi(&style_b, &DEFAULT_STYLE); - assert_eq!( - ansi_difference, - format!( - "{reset}{italic}{white}", - reset = Ansi::RESET, - white = Ansi::rgb(ChatFormatting::WHITE.color.unwrap()), - italic = 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 deleted file mode 100644 index 6c43f8b7..00000000 --- a/minecraft-chat/src/text_component.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::fmt; - -use crate::{base_component::BaseComponent, component::Component, style::ChatFormatting}; - -#[derive(Clone, Debug)] -pub struct TextComponent { - pub base: BaseComponent, - 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_text_component(legacy_color_code: &str) -> TextComponent { - let mut components: Vec<TextComponent> = 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())); - } - - let style = &mut components.last_mut().unwrap().base.style; - // if the formatter is a reset, then we need to reset the style to the default - style.apply_formatting(formatter); - } - 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 -} - -impl<'a> TextComponent { - pub fn new(text: String) -> Self { - // 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) -> Component { - Component::Text(self) - } -} - -impl fmt::Display for TextComponent { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.text.clone()) - } -} - -#[cfg(test)] -mod tests { - use crate::style::Ansi; - - use super::*; - - #[test] - fn test_hypixel_motd() { - let component = - TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_string()) - .get(); - assert_eq!( - component.to_ansi(None), - 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_legacy_color_code_to_component() { - let component = TextComponent::new("§lHello §r§1w§2o§3r§4l§5d".to_string()).get(); - assert_eq!( - component.to_ansi(None), - format!( - "{BOLD}Hello {RESET}{DARK_BLUE}w{DARK_GREEN}o{DARK_AQUA}r{DARK_RED}l{DARK_PURPLE}d{RESET}", - BOLD = Ansi::BOLD, - RESET = Ansi::RESET, - DARK_BLUE = Ansi::rgb(ChatFormatting::DARK_BLUE.color.unwrap()), - DARK_GREEN = Ansi::rgb(ChatFormatting::DARK_GREEN.color.unwrap()), - DARK_AQUA = Ansi::rgb(ChatFormatting::DARK_AQUA.color.unwrap()), - DARK_RED = Ansi::rgb(ChatFormatting::DARK_RED.color.unwrap()), - DARK_PURPLE = Ansi::rgb(ChatFormatting::DARK_PURPLE.color.unwrap()) - ) - ); - } -} diff --git a/minecraft-chat/src/translatable_component.rs b/minecraft-chat/src/translatable_component.rs deleted file mode 100644 index 0709f7bf..00000000 --- a/minecraft-chat/src/translatable_component.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{base_component::BaseComponent, component::Component}; - -#[derive(Clone, Debug)] -pub enum StringOrComponent { - String(String), - Component(Component), -} - -#[derive(Clone, Debug)] -pub struct TranslatableComponent { - pub base: BaseComponent, - pub key: String, - pub args: Vec<StringOrComponent>, -} - -impl TranslatableComponent { - pub fn new(key: String, args: Vec<StringOrComponent>) -> Self { - Self { - base: BaseComponent::new(), - key, - args, - } - } -} diff --git a/minecraft-chat/tests/integration_test.rs b/minecraft-chat/tests/integration_test.rs deleted file mode 100644 index aac12875..00000000 --- a/minecraft-chat/tests/integration_test.rs +++ /dev/null @@ -1,75 +0,0 @@ -use minecraft_chat::{ - component::Component, - style::{Ansi, ChatFormatting, TextColor}, -}; -use serde::Deserialize; -use serde_json::Value; - -#[test] -fn basic_ansi_test() { - let j: Value = serde_json::from_str( - r#"{ - "text": "hello", - "color": "red", - "bold": true -}"#, - ) - .unwrap(); - let component = Component::deserialize(&j).unwrap(); - assert_eq!( - component.to_ansi(None), - "\u{1b}[1m\u{1b}[38;2;255;85;85mhello\u{1b}[m" - ); -} - -#[test] -fn complex_ansi_test() { - let j: Value = serde_json::from_str( - r##"[ - { - "text": "hello", - "color": "red", - "bold": true, - "italic": true, - "underlined": true, - "adsfsf": "this should be ignored", - "extra": [ - {"text": " ", "underlined": false}, - {"text": "world", "bold": false, "strikethrough": true, "color": "#abcdef"} - ] - }, - { - "text": " asdf", - "italic": false, - "obfuscated": "true", - "strikethrough": false - }, - { - "text": "!", - "bold": true - } -]"##, - ) - .unwrap(); - let component = Component::deserialize(&j).unwrap(); - assert_eq!( - component.to_ansi(None), - format!( - "{bold}{italic}{underlined}{red}hello{reset}{bold}{italic}{red} {reset}{italic}{strikethrough}{abcdef}world{reset}{abcdef} asdf{bold}!{reset}", - bold = Ansi::BOLD, - italic = Ansi::ITALIC, - underlined = Ansi::UNDERLINED, - red = Ansi::rgb(ChatFormatting::RED.color.unwrap()), - reset = Ansi::RESET, - strikethrough = Ansi::STRIKETHROUGH, - abcdef = Ansi::rgb(TextColor::parse("#abcdef".to_string()).unwrap().value), - ) - ); -} - -#[test] -fn component_from_string() { - let j: Value = serde_json::from_str("\"foo\"").unwrap(); - let component = Component::deserialize(&j).unwrap(); - assert_eq!(component.to_ansi(None), "foo"); -} |
