aboutsummaryrefslogtreecommitdiff
path: root/azalea-chat/src
diff options
context:
space:
mode:
authormat <git@matdoes.dev>2025-08-12 17:39:05 +1200
committermat <git@matdoes.dev>2025-08-12 17:39:05 +1200
commit12aeae07d2667cd188acf88190ed5c3f7983926a (patch)
tree4f4549b8e37d76f098d4164398517813bb0dfcc6 /azalea-chat/src
parentfa1050d6eedf80bbc20e91163b99c0306414e627 (diff)
downloadazalea-drasl-12aeae07d2667cd188acf88190ed5c3f7983926a.tar.xz
fix wrong chat styling sometimes when 'extra' field is used
Diffstat (limited to 'azalea-chat/src')
-rw-r--r--azalea-chat/src/base_component.rs1
-rw-r--r--azalea-chat/src/click_event.rs31
-rw-r--r--azalea-chat/src/component.rs161
-rw-r--r--azalea-chat/src/style.rs202
-rw-r--r--azalea-chat/src/text_component.rs47
5 files changed, 211 insertions, 231 deletions
diff --git a/azalea-chat/src/base_component.rs b/azalea-chat/src/base_component.rs
index 366904c7..27666c17 100644
--- a/azalea-chat/src/base_component.rs
+++ b/azalea-chat/src/base_component.rs
@@ -5,6 +5,7 @@ use crate::{FormattedText, style::Style};
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct BaseComponent {
// implements mutablecomponent
+ /// Components in the "extra" field.
#[serde(skip_serializing_if = "Vec::is_empty")]
pub siblings: Vec<FormattedText>,
#[serde(flatten)]
diff --git a/azalea-chat/src/click_event.rs b/azalea-chat/src/click_event.rs
index 765ef3ef..dddaff74 100644
--- a/azalea-chat/src/click_event.rs
+++ b/azalea-chat/src/click_event.rs
@@ -1,16 +1,33 @@
use serde::Serialize;
+#[cfg(feature = "simdnbt")]
use simdnbt::owned::Nbt;
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case", tag = "action")]
pub enum ClickEvent {
- OpenUrl { url: String },
- OpenFile { path: String },
- RunCommand { command: String },
- SuggestCommand { command: String },
+ OpenUrl {
+ url: String,
+ },
+ OpenFile {
+ path: String,
+ },
+ RunCommand {
+ command: String,
+ },
+ SuggestCommand {
+ command: String,
+ },
// TODO: this uses Dialog.CODEC
ShowDialog,
- ChangePage { page: i32 },
- CopyToClipboard { value: String },
- Custom { id: String, payload: Nbt },
+ ChangePage {
+ page: i32,
+ },
+ CopyToClipboard {
+ value: String,
+ },
+ Custom {
+ id: String,
+ #[cfg(feature = "simdnbt")]
+ payload: Nbt,
+ },
}
diff --git a/azalea-chat/src/component.rs b/azalea-chat/src/component.rs
index e1652cf1..bf064c8f 100644
--- a/azalea-chat/src/component.rs
+++ b/azalea-chat/src/component.rs
@@ -99,18 +99,18 @@ impl FormattedText {
/// })).unwrap();
///
/// let ansi = component.to_custom_format(
- /// |running, new, default| (running.compare_ansi(new, default), String::new()),
+ /// |running, new| (running.compare_ansi(new), "".to_string()),
/// |text| text.to_string(),
/// |style| {
/// if !style.is_empty() {
/// "\u{1b}[m".to_string()
/// } else {
- /// String::new()
+ /// "".to_string()
/// }
/// },
/// &DEFAULT_STYLE,
/// );
- /// println!("{}", ansi);
+ /// println!("{ansi}");
/// ```
pub fn to_custom_format<F, S, C>(
&self,
@@ -120,42 +120,72 @@ impl FormattedText {
default_style: &Style,
) -> String
where
- F: FnMut(&Style, &Style, &Style) -> (String, String),
+ F: FnMut(&Style, &Style) -> (String, String),
S: FnMut(&str) -> String,
C: FnMut(&Style) -> String,
{
let mut output = String::new();
+
let mut running_style = Style::default();
+ self.to_custom_format_recursive(
+ &mut output,
+ &mut style_formatter,
+ &mut text_formatter,
+ &mut cleanup_formatter,
+ &default_style.clone(),
+ &mut running_style,
+ );
+ output.push_str(&cleanup_formatter(&running_style));
- for component in self.clone().into_iter() {
- let component_text = match &component {
- Self::Text(c) => c.text.to_string(),
- Self::Translatable(c) => match c.read() {
- Ok(c) => c.to_string(),
- Err(_) => c.key.to_string(),
- },
- };
+ output
+ }
+
+ fn to_custom_format_recursive<F, S, C>(
+ &self,
+ output: &mut String,
+ style_formatter: &mut F,
+ text_formatter: &mut S,
+ cleanup_formatter: &mut C,
+ parent_style: &Style,
+ running_style: &mut Style,
+ ) where
+ F: FnMut(&Style, &Style) -> (String, String),
+ S: FnMut(&str) -> String,
+ C: FnMut(&Style) -> String,
+ {
+ let component_text = match &self {
+ Self::Text(c) => c.text.to_string(),
+ Self::Translatable(c) => match c.read() {
+ Ok(c) => c.to_string(),
+ Err(_) => c.key.to_string(),
+ },
+ };
- let component_style = &component.get_base().style;
+ let component_style = &self.get_base().style;
+ let new_style = parent_style.merged_with(component_style);
- let formatted_style = style_formatter(&running_style, component_style, default_style);
+ if !component_text.is_empty() {
+ let (formatted_style_prefix, formatted_style_suffix) =
+ style_formatter(&running_style, &new_style);
let formatted_text = text_formatter(&component_text);
- output.push_str(&formatted_style.0);
+ output.push_str(&formatted_style_prefix);
output.push_str(&formatted_text);
- output.push_str(&formatted_style.1);
+ output.push_str(&formatted_style_suffix);
- // Reset running style if required
- if component_style.reset {
- running_style = default_style.clone();
- } else {
- running_style.apply(component_style);
- }
+ *running_style = new_style.clone();
}
- output.push_str(&cleanup_formatter(&running_style));
-
- output
+ for sibling in &self.get_base().siblings {
+ sibling.to_custom_format_recursive(
+ output,
+ style_formatter,
+ text_formatter,
+ cleanup_formatter,
+ &new_style,
+ running_style,
+ );
+ }
}
/// Convert this component into an
@@ -165,7 +195,7 @@ impl FormattedText {
/// default [`Style`] to use.
pub fn to_ansi_with_custom_style(&self, default_style: &Style) -> String {
self.to_custom_format(
- |running, new, default| (running.compare_ansi(new, default), "".to_owned()),
+ |running, new| (running.compare_ansi(new), "".to_owned()),
|text| text.to_string(),
|style| if !style.is_empty() { "\u{1b}[m" } else { "" }.to_string(),
default_style,
@@ -180,6 +210,9 @@ impl FormattedText {
/// [`FormattedText::to_ansi_with_custom_style`] with a default [`Style`]
/// colored white.
///
+ /// If you don't want the result to be styled at all, use
+ /// [`Self::to_string`].
+ ///
/// # Examples
///
/// ```rust
@@ -199,7 +232,7 @@ impl FormattedText {
pub fn to_html(&self) -> String {
self.to_custom_format(
- |running, new, _| {
+ |running, new| {
(
format!(
"<span style=\"{}\">",
@@ -222,6 +255,9 @@ impl FormattedText {
}
impl IntoIterator for FormattedText {
+ type Item = FormattedText;
+ type IntoIter = std::vec::IntoIter<Self::Item>;
+
/// Recursively call the function for every component in this component
fn into_iter(self) -> Self::IntoIter {
let base = self.get_base();
@@ -234,9 +270,6 @@ impl IntoIterator for FormattedText {
v.into_iter()
}
-
- type Item = FormattedText;
- type IntoIter = std::vec::IntoIter<Self::Item>;
}
impl<'de> Deserialize<'de> for FormattedText {
@@ -388,7 +421,7 @@ impl simdnbt::FromNbtTag for FormattedText {
fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
// if it's a string, return a text component with that string
if let Some(string) = tag.string() {
- Some(FormattedText::from(string))
+ Some(FormattedText::from_nbt_string(string))
}
// if it's a compound, make it do things with { text } and stuff
// simdnbt::borrow::NbtTag::Compound(compound) => {
@@ -397,22 +430,7 @@ impl simdnbt::FromNbtTag for FormattedText {
}
// ok so it's not a compound, if it's a list deserialize every item
else if let Some(list) = tag.list() {
- let mut component;
- if let Some(compounds) = list.compounds() {
- component = FormattedText::from_nbt_compound(compounds.first()?)?;
- for compound in compounds.into_iter().skip(1) {
- component.append(FormattedText::from_nbt_compound(compound)?);
- }
- } else if let Some(strings) = list.strings() {
- component = FormattedText::from(*(strings.first()?));
- for &string in strings.iter().skip(1) {
- component.append(FormattedText::from(string));
- }
- } else {
- debug!("couldn't parse {list:?} as FormattedText");
- return None;
- }
- Some(component)
+ FormattedText::from_nbt_list(list)
} else {
Some(FormattedText::Text(TextComponent::new("".to_owned())))
}
@@ -421,6 +439,28 @@ impl simdnbt::FromNbtTag for FormattedText {
#[cfg(feature = "simdnbt")]
impl FormattedText {
+ fn from_nbt_string(s: &simdnbt::Mutf8Str) -> Self {
+ FormattedText::from(s)
+ }
+ fn from_nbt_list(list: simdnbt::borrow::NbtList) -> Option<FormattedText> {
+ let mut component;
+ if let Some(compounds) = list.compounds() {
+ component = FormattedText::from_nbt_compound(compounds.first()?)?;
+ for compound in compounds.into_iter().skip(1) {
+ component.append(FormattedText::from_nbt_compound(compound)?);
+ }
+ } else if let Some(strings) = list.strings() {
+ component = FormattedText::from(*(strings.first()?));
+ for &string in strings.iter().skip(1) {
+ component.append(FormattedText::from(string));
+ }
+ } else {
+ debug!("couldn't parse {list:?} as FormattedText");
+ return None;
+ }
+ Some(component)
+ }
+
pub fn from_nbt_compound(compound: simdnbt::borrow::NbtCompound) -> Option<Self> {
let mut component: FormattedText;
@@ -534,7 +574,28 @@ impl FormattedText {
return None;
}
if let Some(extra) = compound.get("extra") {
- component.append(FormattedText::from_nbt_tag(extra)?);
+ // if it's an array, deserialize every item
+ if let Some(items) = extra.list() {
+ if let Some(items) = items.compounds() {
+ for item in items {
+ component.append(FormattedText::from_nbt_compound(item)?);
+ }
+ } else if let Some(items) = items.strings() {
+ for item in items {
+ component.append(FormattedText::from_nbt_string(item));
+ }
+ } else if let Some(items) = items.lists() {
+ for item in items {
+ component.append(FormattedText::from_nbt_list(item)?);
+ }
+ } else {
+ warn!(
+ "couldn't parse {items:?} as FormattedText because it's not a list of compounds or strings"
+ );
+ }
+ } else {
+ component.append(FormattedText::from_nbt_tag(extra)?);
+ }
}
let base_style = Style::from_compound(compound).ok()?;
@@ -556,6 +617,10 @@ impl From<&simdnbt::Mutf8Str> for FormattedText {
impl AzaleaRead for FormattedText {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let nbt = simdnbt::borrow::read_optional_tag(buf)?;
+ trace!(
+ "Reading NBT for FormattedText: {:?}",
+ nbt.as_ref().map(|n| n.as_tag().to_owned())
+ );
match nbt {
Some(nbt) => FormattedText::from_nbt_tag(nbt.as_tag()).ok_or(BufReadError::Custom(
"couldn't convert nbt to chat message".to_owned(),
diff --git a/azalea-chat/src/style.rs b/azalea-chat/src/style.rs
index ebab6874..1ae0e3db 100644
--- a/azalea-chat/src/style.rs
+++ b/azalea-chat/src/style.rs
@@ -2,7 +2,7 @@ use std::{collections::HashMap, fmt, sync::LazyLock};
#[cfg(feature = "azalea-buf")]
use azalea_buf::AzBuf;
-use serde::{Serialize, Serializer, ser::SerializeStruct};
+use serde::{Serialize, Serializer};
use serde_json::Value;
#[cfg(feature = "simdnbt")]
use simdnbt::owned::{NbtCompound, NbtTag};
@@ -83,6 +83,7 @@ impl Ansi {
pub const ITALIC: &'static str = "\u{1b}[3m";
pub const UNDERLINED: &'static str = "\u{1b}[4m";
pub const STRIKETHROUGH: &'static str = "\u{1b}[9m";
+ // "Conceal or hide"
pub const OBFUSCATED: &'static str = "\u{1b}[8m";
pub const RESET: &'static str = "\u{1b}[m";
@@ -303,23 +304,32 @@ impl TryFrom<ChatFormatting> for TextColor {
}
}
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Clone, Debug, Default, PartialEq, serde::Serialize)]
#[non_exhaustive]
pub struct Style {
+ #[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<TextColor>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub shadow_color: Option<u32>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub bold: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub italic: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub underlined: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub strikethrough: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub obfuscated: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub click_event: Option<ClickEvent>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub hover_event: Option<HoverEvent>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub insertion: Option<String>,
/// Represented as a `ResourceLocation`.
+ #[serde(skip_serializing_if = "Option::is_none")]
pub font: Option<String>,
- /// Whether formatting should be reset before applying these styles
- pub reset: bool,
}
impl Style {
pub fn new() -> Self {
@@ -369,25 +379,6 @@ impl Style {
self.font = font.into();
self
}
- pub fn reset(mut self, reset: bool) -> Self {
- self.reset = reset;
- self
- }
-}
-
-fn serde_serialize_field<S: serde::ser::SerializeStruct>(
- state: &mut S,
- name: &'static str,
- value: &Option<impl serde::Serialize>,
- default: &(impl serde::Serialize + ?Sized),
- reset: bool,
-) -> Result<(), S::Error> {
- if let Some(value) = value {
- state.serialize_field(name, value)?;
- } else if reset {
- state.serialize_field(name, default)?;
- }
- Ok(())
}
#[cfg(feature = "simdnbt")]
@@ -395,64 +386,9 @@ fn simdnbt_serialize_field(
compound: &mut simdnbt::owned::NbtCompound,
name: &'static str,
value: Option<impl simdnbt::ToNbtTag>,
- default: impl simdnbt::ToNbtTag,
- reset: bool,
) {
- match value {
- Some(value) => {
- compound.insert(name, value);
- }
- _ => {
- if reset {
- compound.insert(name, default);
- }
- }
- }
-}
-
-impl Serialize for Style {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: Serializer,
- {
- let len = if self.reset {
- 6
- } else {
- usize::from(self.color.is_some())
- + usize::from(self.bold.is_some())
- + usize::from(self.italic.is_some())
- + usize::from(self.underlined.is_some())
- + usize::from(self.strikethrough.is_some())
- + usize::from(self.obfuscated.is_some())
- };
- let mut state = serializer.serialize_struct("Style", len)?;
-
- serde_serialize_field(&mut state, "color", &self.color, "white", self.reset)?;
- serde_serialize_field(&mut state, "bold", &self.bold, &false, self.reset)?;
- serde_serialize_field(&mut state, "italic", &self.italic, &false, self.reset)?;
- serde_serialize_field(
- &mut state,
- "underlined",
- &self.underlined,
- &false,
- self.reset,
- )?;
- serde_serialize_field(
- &mut state,
- "strikethrough",
- &self.strikethrough,
- &false,
- self.reset,
- )?;
- serde_serialize_field(
- &mut state,
- "obfuscated",
- &self.obfuscated,
- &false,
- self.reset,
- )?;
-
- state.end()
+ if let Some(value) = value {
+ compound.insert(name, value);
}
}
@@ -461,30 +397,12 @@ impl simdnbt::Serialize for Style {
fn to_compound(self) -> NbtCompound {
let mut compound = NbtCompound::new();
- simdnbt_serialize_field(&mut compound, "color", self.color, "white", self.reset);
- simdnbt_serialize_field(&mut compound, "bold", self.bold, false, self.reset);
- simdnbt_serialize_field(&mut compound, "italic", self.italic, false, self.reset);
- simdnbt_serialize_field(
- &mut compound,
- "underlined",
- self.underlined,
- false,
- self.reset,
- );
- simdnbt_serialize_field(
- &mut compound,
- "strikethrough",
- self.strikethrough,
- false,
- self.reset,
- );
- simdnbt_serialize_field(
- &mut compound,
- "obfuscated",
- self.obfuscated,
- false,
- self.reset,
- );
+ simdnbt_serialize_field(&mut compound, "color", self.color);
+ simdnbt_serialize_field(&mut compound, "bold", self.bold);
+ simdnbt_serialize_field(&mut compound, "italic", self.italic);
+ simdnbt_serialize_field(&mut compound, "underlined", self.underlined);
+ simdnbt_serialize_field(&mut compound, "strikethrough", self.strikethrough);
+ simdnbt_serialize_field(&mut compound, "obfuscated", self.obfuscated);
compound
}
@@ -530,55 +448,49 @@ impl Style {
}
/// 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 ||
+ pub fn compare_ansi(&self, after: &Style) -> String {
+ let should_reset =
// if it used to be bold and now it's not, reset
- (self.bold.unwrap_or(false) && !after.bold.unwrap_or(true)) ||
+ (self.bold.unwrap_or_default() && !after.bold.unwrap_or_default()) ||
// if it used to be italic and now it's not, reset
- (self.italic.unwrap_or(false) && !after.italic.unwrap_or(true)) ||
+ (self.italic.unwrap_or_default() && !after.italic.unwrap_or_default()) ||
// if it used to be underlined and now it's not, reset
- (self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true)) ||
+ (self.underlined.unwrap_or_default() && !after.underlined.unwrap_or_default()) ||
// if it used to be strikethrough and now it's not, reset
- (self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true)) ||
+ (self.strikethrough.unwrap_or_default() && !after.strikethrough.unwrap_or_default()) ||
// if it used to be obfuscated and now it's not, reset
- (self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true));
+ (self.obfuscated.unwrap_or_default() && !after.obfuscated.unwrap_or_default());
let mut ansi_codes = String::new();
let empty_style = Style::empty();
- let (before, after) = if should_reset {
+ let before = 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)
+ &empty_style
} else {
- (self, after.clone())
+ self
};
// 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) {
+ if !before.bold.unwrap_or_default() && after.bold.unwrap_or_default() {
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) {
+ if !before.italic.unwrap_or_default() && after.italic.unwrap_or_default() {
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) {
+ if !before.underlined.unwrap_or_default() && after.underlined.unwrap_or_default() {
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) {
+ if !before.strikethrough.unwrap_or_default() && after.strikethrough.unwrap_or_default() {
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) {
+ if !before.obfuscated.unwrap_or_default() && after.obfuscated.unwrap_or_default() {
ansi_codes.push_str(Ansi::OBFUSCATED);
}
@@ -639,7 +551,6 @@ impl Style {
hover_event: other.hover_event.clone().or(self.hover_event.clone()),
insertion: other.insertion.clone().or(self.insertion.clone()),
font: other.font.clone().or(self.font.clone()),
- reset: other.reset, // if reset is true in the new style, that takes precedence
}
}
@@ -651,7 +562,14 @@ impl Style {
ChatFormatting::Underline => self.underlined = Some(true),
ChatFormatting::Strikethrough => self.strikethrough = Some(true),
ChatFormatting::Obfuscated => self.obfuscated = Some(true),
- ChatFormatting::Reset => self.reset = true,
+ ChatFormatting::Reset => {
+ self.color = None;
+ self.bold = None;
+ self.italic = None;
+ self.underlined = None;
+ self.strikethrough = None;
+ self.obfuscated = None;
+ }
formatter => {
// if it's a color, set it
if let Some(color) = formatter.color() {
@@ -732,7 +650,6 @@ impl simdnbt::Deserialize for Style {
#[cfg(test)]
mod tests {
use super::*;
- use crate::component::DEFAULT_STYLE;
#[test]
fn text_color_named_colors() {
@@ -752,9 +669,10 @@ mod tests {
};
let style_b = Style {
bold: Some(false),
+ italic: Some(true),
..Style::default()
};
- let ansi_difference = style_a.compare_ansi(&style_b, &Style::default());
+ let ansi_difference = style_a.compare_ansi(&style_b);
assert_eq!(
ansi_difference,
format!(
@@ -771,37 +689,15 @@ mod tests {
..Style::default()
};
let style_b = Style {
+ bold: Some(true),
italic: Some(true),
..Style::default()
};
- let ansi_difference = style_a.compare_ansi(&style_b, &Style::default());
+ let ansi_difference = style_a.compare_ansi(&style_b);
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(),
diff --git a/azalea-chat/src/text_component.rs b/azalea-chat/src/text_component.rs
index eab7ee79..962ec46e 100644
--- a/azalea-chat/src/text_component.rs
+++ b/azalea-chat/src/text_component.rs
@@ -61,11 +61,17 @@ const LEGACY_FORMATTING_CODE_SYMBOL: char = 'ยง';
/// 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<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
+ 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;
@@ -84,41 +90,35 @@ pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextCompo
.take(7)
.collect::<String>();
- 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;
- style.color = TextColor::parse(&color);
+ if !cur_component.text.is_empty() {
+ // we need to split this into a new component
+ components.push(cur_component.clone());
+ cur_component.text = "".to_string();
+ };
+ cur_component.base.style.color = TextColor::parse(&color);
i += 6;
} else if let Some(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;
- style.apply_formatting(&formatter);
+ 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_string();
+ };
+ cur_component.base.style.apply_formatting(&formatter);
}
i += 1;
} else {
- if components.is_empty() {
- components.push(TextComponent::new("".to_string()));
- }
- components
- .last_mut()
- .unwrap()
+ cur_component
.text
.push(legacy_color_code.chars().nth(i).unwrap());
};
i += 1;
}
- if components.is_empty() {
- return TextComponent::new("".to_string());
- }
+ components.push(cur_component);
- // 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);
+ // 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());
}
@@ -227,8 +227,9 @@ mod tests {
assert_eq!(
component.to_ansi(),
format!(
- "{BOLD}Hello {RESET}{DARK_BLUE}w{DARK_GREEN}o{DARK_AQUA}r{DARK_RED}l{DARK_PURPLE}d{RESET}",
+ "{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()),