azalea_chat/
component.rs

1use std::{fmt::Display, sync::LazyLock};
2
3#[cfg(feature = "azalea-buf")]
4use azalea_buf::{AzaleaRead, AzaleaWrite, BufReadError};
5use serde::{de, Deserialize, Deserializer, Serialize};
6#[cfg(feature = "simdnbt")]
7use simdnbt::{Deserialize as _, FromNbtTag as _, Serialize as _};
8use tracing::{debug, trace, warn};
9
10use crate::{
11    base_component::BaseComponent,
12    style::{ChatFormatting, Style},
13    text_component::TextComponent,
14    translatable_component::{StringOrComponent, TranslatableComponent},
15};
16
17/// A chat component, basically anything you can see in chat.
18#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)]
19#[serde(untagged)]
20pub enum FormattedText {
21    Text(TextComponent),
22    Translatable(TranslatableComponent),
23}
24
25pub static DEFAULT_STYLE: LazyLock<Style> = LazyLock::new(|| Style {
26    color: Some(ChatFormatting::White.try_into().unwrap()),
27    ..Style::default()
28});
29
30/// A chat component
31impl FormattedText {
32    pub fn get_base_mut(&mut self) -> &mut BaseComponent {
33        match self {
34            Self::Text(c) => &mut c.base,
35            Self::Translatable(c) => &mut c.base,
36        }
37    }
38
39    pub fn get_base(&self) -> &BaseComponent {
40        match self {
41            Self::Text(c) => &c.base,
42            Self::Translatable(c) => &c.base,
43        }
44    }
45
46    /// Add a component as a sibling of this one
47    fn append(&mut self, sibling: FormattedText) {
48        self.get_base_mut().siblings.push(sibling);
49    }
50
51    /// Get the "separator" component from the json
52    fn parse_separator(
53        json: &serde_json::Value,
54    ) -> Result<Option<FormattedText>, serde_json::Error> {
55        if let Some(separator) = json.get("separator") {
56            return Ok(Some(FormattedText::deserialize(separator)?));
57        }
58        Ok(None)
59    }
60
61    #[cfg(feature = "simdnbt")]
62    fn parse_separator_nbt(nbt: &simdnbt::borrow::NbtCompound) -> Option<FormattedText> {
63        if let Some(separator) = nbt.get("separator") {
64            FormattedText::from_nbt_tag(separator)
65        } else {
66            None
67        }
68    }
69
70    /// Convert this component into an
71    /// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code), so you
72    /// can print it to your terminal and get styling.
73    ///
74    /// This is technically a shortcut for
75    /// [`FormattedText::to_ansi_with_custom_style`] with a default [`Style`]
76    /// colored white.
77    ///
78    /// # Examples
79    ///
80    /// ```rust
81    /// use azalea_chat::FormattedText;
82    /// use serde::de::Deserialize;
83    ///
84    /// let component = FormattedText::deserialize(&serde_json::json!({
85    ///    "text": "Hello, world!",
86    ///    "color": "red",
87    /// })).unwrap();
88    ///
89    /// println!("{}", component.to_ansi());
90    /// ```
91    pub fn to_ansi(&self) -> String {
92        // default the default_style to white if it's not set
93        self.to_ansi_with_custom_style(&DEFAULT_STYLE)
94    }
95
96    /// Convert this component into an
97    /// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code).
98    ///
99    /// This is the same as [`FormattedText::to_ansi`], but you can specify a
100    /// default [`Style`] to use.
101    pub fn to_ansi_with_custom_style(&self, default_style: &Style) -> String {
102        // this contains the final string will all the ansi escape codes
103        let mut built_string = String::new();
104        // this style will update as we visit components
105        let mut running_style = Style::default();
106
107        for component in self.clone().into_iter() {
108            let component_text = match &component {
109                Self::Text(c) => c.text.to_string(),
110                Self::Translatable(c) => c.to_string(),
111            };
112
113            let component_style = &component.get_base().style;
114
115            let ansi_text = running_style.compare_ansi(component_style, default_style);
116            built_string.push_str(&ansi_text);
117            built_string.push_str(&component_text);
118
119            running_style.apply(component_style);
120        }
121
122        if !running_style.is_empty() {
123            built_string.push_str("\u{1b}[m");
124        }
125
126        built_string
127    }
128}
129
130impl IntoIterator for FormattedText {
131    /// Recursively call the function for every component in this component
132    fn into_iter(self) -> Self::IntoIter {
133        let base = self.get_base();
134        let siblings = base.siblings.clone();
135        let mut v: Vec<FormattedText> = Vec::with_capacity(siblings.len() + 1);
136        v.push(self);
137        for sibling in siblings {
138            v.extend(sibling);
139        }
140
141        v.into_iter()
142    }
143
144    type Item = FormattedText;
145    type IntoIter = std::vec::IntoIter<Self::Item>;
146}
147
148impl<'de> Deserialize<'de> for FormattedText {
149    fn deserialize<D>(de: D) -> Result<Self, D::Error>
150    where
151        D: Deserializer<'de>,
152    {
153        let json: serde_json::Value = serde::Deserialize::deserialize(de)?;
154
155        // we create a component that we might add siblings to
156        let mut component: FormattedText;
157
158        // if it's primitive, make it a text component
159        if !json.is_array() && !json.is_object() {
160            return Ok(FormattedText::Text(TextComponent::new(
161                json.as_str().unwrap_or("").to_string(),
162            )));
163        }
164        // if it's an object, do things with { text } and stuff
165        else if json.is_object() {
166            if let Some(text) = json.get("text") {
167                let text = text.as_str().unwrap_or("").to_string();
168                component = FormattedText::Text(TextComponent::new(text));
169            } else if let Some(translate) = json.get("translate") {
170                let translate = translate
171                    .as_str()
172                    .ok_or_else(|| de::Error::custom("\"translate\" must be a string"))?
173                    .into();
174                if let Some(with) = json.get("with") {
175                    let with = with
176                        .as_array()
177                        .ok_or_else(|| de::Error::custom("\"with\" must be an array"))?;
178                    let mut with_array = Vec::with_capacity(with.len());
179                    for item in with {
180                        // if it's a string component with no styling and no siblings, just add a
181                        // string to with_array otherwise add the component
182                        // to the array
183                        let c = FormattedText::deserialize(item).map_err(de::Error::custom)?;
184                        if let FormattedText::Text(text_component) = c {
185                            if text_component.base.siblings.is_empty()
186                                && text_component.base.style.is_empty()
187                            {
188                                with_array.push(StringOrComponent::String(text_component.text));
189                                continue;
190                            }
191                        }
192                        with_array.push(StringOrComponent::FormattedText(
193                            FormattedText::deserialize(item).map_err(de::Error::custom)?,
194                        ));
195                    }
196                    component = FormattedText::Translatable(TranslatableComponent::new(
197                        translate, with_array,
198                    ));
199                } else {
200                    // if it doesn't have a "with", just have the with_array be empty
201                    component = FormattedText::Translatable(TranslatableComponent::new(
202                        translate,
203                        Vec::new(),
204                    ));
205                }
206            } else if let Some(score) = json.get("score") {
207                // object = GsonHelper.getAsJsonObject(jsonObject, "score");
208                if score.get("name").is_none() || score.get("objective").is_none() {
209                    return Err(de::Error::missing_field(
210                        "A score component needs at least a name and an objective",
211                    ));
212                }
213                // TODO
214                return Err(de::Error::custom(
215                    "score text components aren't yet supported",
216                ));
217            } else if json.get("selector").is_some() {
218                return Err(de::Error::custom(
219                    "selector text components aren't yet supported",
220                ));
221            } else if json.get("keybind").is_some() {
222                return Err(de::Error::custom(
223                    "keybind text components aren't yet supported",
224                ));
225            } else {
226                let Some(_nbt) = json.get("nbt") else {
227                    return Err(de::Error::custom(
228                        format!("Don't know how to turn {json} into a FormattedText").as_str(),
229                    ));
230                };
231                let _separator =
232                    FormattedText::parse_separator(&json).map_err(de::Error::custom)?;
233
234                let _interpret = match json.get("interpret") {
235                    Some(v) => v.as_bool().ok_or(Some(false)).unwrap(),
236                    None => false,
237                };
238                if let Some(_block) = json.get("block") {}
239                return Err(de::Error::custom(
240                    "nbt text components aren't yet supported",
241                ));
242            }
243            if let Some(extra) = json.get("extra") {
244                let Some(extra) = extra.as_array() else {
245                    return Err(de::Error::custom("Extra isn't an array"));
246                };
247                if extra.is_empty() {
248                    return Err(de::Error::custom("Unexpected empty array of components"));
249                }
250                for extra_component in extra {
251                    let sibling =
252                        FormattedText::deserialize(extra_component).map_err(de::Error::custom)?;
253                    component.append(sibling);
254                }
255            }
256
257            let style = Style::deserialize(&json);
258            component.get_base_mut().style = style;
259
260            return Ok(component);
261        }
262        // ok so it's not an object, if it's an array deserialize every item
263        else if !json.is_array() {
264            return Err(de::Error::custom(
265                format!("Don't know how to turn {json} into a FormattedText").as_str(),
266            ));
267        }
268        let json_array = json.as_array().unwrap();
269        // the first item in the array is the one that we're gonna return, the others
270        // are siblings
271        let mut component =
272            FormattedText::deserialize(&json_array[0]).map_err(de::Error::custom)?;
273        for i in 1..json_array.len() {
274            component.append(
275                FormattedText::deserialize(json_array.get(i).unwrap())
276                    .map_err(de::Error::custom)?,
277            );
278        }
279        Ok(component)
280    }
281}
282
283#[cfg(feature = "simdnbt")]
284impl simdnbt::Serialize for FormattedText {
285    fn to_compound(self) -> simdnbt::owned::NbtCompound {
286        match self {
287            FormattedText::Text(c) => c.to_compound(),
288            FormattedText::Translatable(c) => c.to_compound(),
289        }
290    }
291}
292
293#[cfg(feature = "simdnbt")]
294impl simdnbt::FromNbtTag for FormattedText {
295    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
296        // if it's a string, return a text component with that string
297        if let Some(string) = tag.string() {
298            Some(FormattedText::from(string))
299        }
300        // if it's a compound, make it do things with { text } and stuff
301        // simdnbt::borrow::NbtTag::Compound(compound) => {
302        else if let Some(compound) = tag.compound() {
303            FormattedText::from_nbt_compound(compound)
304        }
305        // ok so it's not a compound, if it's a list deserialize every item
306        else if let Some(list) = tag.list() {
307            let mut component;
308            if let Some(compounds) = list.compounds() {
309                component = FormattedText::from_nbt_compound(compounds.first()?)?;
310                for compound in compounds.into_iter().skip(1) {
311                    component.append(FormattedText::from_nbt_compound(compound)?);
312                }
313            } else if let Some(strings) = list.strings() {
314                component = FormattedText::from(*(strings.first()?));
315                for &string in strings.iter().skip(1) {
316                    component.append(FormattedText::from(string));
317                }
318            } else {
319                debug!("couldn't parse {list:?} as FormattedText");
320                return None;
321            }
322            Some(component)
323        } else {
324            Some(FormattedText::Text(TextComponent::new("".to_owned())))
325        }
326    }
327}
328
329#[cfg(feature = "simdnbt")]
330impl FormattedText {
331    pub fn from_nbt_compound(compound: simdnbt::borrow::NbtCompound) -> Option<Self> {
332        let mut component: FormattedText;
333
334        if let Some(text) = compound.get("text") {
335            let text = text.string().unwrap_or_default().to_string();
336            component = FormattedText::Text(TextComponent::new(text));
337        } else if let Some(translate) = compound.get("translate") {
338            let translate = translate.string()?.into();
339            if let Some(with) = compound.get("with") {
340                let mut with_array = Vec::new();
341                let with_list = with.list()?;
342                if with_list.empty() {
343                } else if let Some(with) = with_list.strings() {
344                    for item in with {
345                        with_array.push(StringOrComponent::String(item.to_string()));
346                    }
347                } else if let Some(with) = with_list.compounds() {
348                    for item in with {
349                        // if it's a string component with no styling and no siblings,
350                        // just add a string to
351                        // with_array otherwise add the
352                        // component to the array
353                        if let Some(primitive) = item.get("") {
354                            // minecraft does this sometimes, for example
355                            // for the /give system messages
356                            if let Some(b) = primitive.byte() {
357                                // interpreted as boolean
358                                with_array.push(StringOrComponent::String(
359                                    if b != 0 { "true" } else { "false" }.to_string(),
360                                ));
361                            } else if let Some(s) = primitive.short() {
362                                with_array.push(StringOrComponent::String(s.to_string()));
363                            } else if let Some(i) = primitive.int() {
364                                with_array.push(StringOrComponent::String(i.to_string()));
365                            } else if let Some(l) = primitive.long() {
366                                with_array.push(StringOrComponent::String(l.to_string()));
367                            } else if let Some(f) = primitive.float() {
368                                with_array.push(StringOrComponent::String(f.to_string()));
369                            } else if let Some(d) = primitive.double() {
370                                with_array.push(StringOrComponent::String(d.to_string()));
371                            } else if let Some(s) = primitive.string() {
372                                with_array.push(StringOrComponent::String(s.to_string()));
373                            } else {
374                                warn!("couldn't parse {item:?} as FormattedText because it has a disallowed primitive");
375                                with_array.push(StringOrComponent::String("?".to_string()));
376                            }
377                        } else if let Some(c) = FormattedText::from_nbt_compound(item) {
378                            if let FormattedText::Text(text_component) = c {
379                                if text_component.base.siblings.is_empty()
380                                    && text_component.base.style.is_empty()
381                                {
382                                    with_array.push(StringOrComponent::String(text_component.text));
383                                    continue;
384                                }
385                            }
386                            with_array.push(StringOrComponent::FormattedText(
387                                FormattedText::from_nbt_compound(item)?,
388                            ));
389                        } else {
390                            warn!("couldn't parse {item:?} as FormattedText");
391                            with_array.push(StringOrComponent::String("?".to_string()));
392                        }
393                    }
394                } else {
395                    warn!("couldn't parse {with:?} as FormattedText because it's not a list of compounds");
396                    return None;
397                }
398                component =
399                    FormattedText::Translatable(TranslatableComponent::new(translate, with_array));
400            } else {
401                // if it doesn't have a "with", just have the with_array be empty
402                component =
403                    FormattedText::Translatable(TranslatableComponent::new(translate, Vec::new()));
404            }
405        } else if let Some(score) = compound.compound("score") {
406            // object = GsonHelper.getAsJsonObject(jsonObject, "score");
407            if score.get("name").is_none() || score.get("objective").is_none() {
408                // A score component needs at least a name and an objective
409                trace!("A score component needs at least a name and an objective");
410                return None;
411            }
412            // TODO, score text components aren't yet supported
413            return None;
414        } else if compound.get("selector").is_some() {
415            // selector text components aren't yet supported
416            trace!("selector text components aren't yet supported");
417            return None;
418        } else if compound.get("keybind").is_some() {
419            // keybind text components aren't yet supported
420            trace!("keybind text components aren't yet supported");
421            return None;
422        } else if let Some(tag) = compound.get("") {
423            return FormattedText::from_nbt_tag(tag);
424        } else {
425            let _nbt = compound.get("nbt")?;
426            let _separator = FormattedText::parse_separator_nbt(&compound)?;
427
428            let _interpret = match compound.get("interpret") {
429                Some(v) => v.byte().unwrap_or_default() != 0,
430                None => false,
431            };
432            if let Some(_block) = compound.get("block") {}
433            trace!("nbt text components aren't yet supported");
434            return None;
435        }
436        if let Some(extra) = compound.get("extra") {
437            component.append(FormattedText::from_nbt_tag(extra)?);
438        }
439
440        let style = Style::from_compound(compound).ok()?;
441        component.get_base_mut().style = style;
442
443        Some(component)
444    }
445}
446
447#[cfg(feature = "simdnbt")]
448impl From<&simdnbt::Mutf8Str> for FormattedText {
449    fn from(s: &simdnbt::Mutf8Str) -> Self {
450        FormattedText::Text(TextComponent::new(s.to_string()))
451    }
452}
453
454#[cfg(feature = "azalea-buf")]
455#[cfg(feature = "simdnbt")]
456impl AzaleaRead for FormattedText {
457    fn azalea_read(buf: &mut std::io::Cursor<&[u8]>) -> Result<Self, BufReadError> {
458        let nbt = simdnbt::borrow::read_optional_tag(buf)?;
459        if let Some(nbt) = nbt {
460            FormattedText::from_nbt_tag(nbt.as_tag()).ok_or(BufReadError::Custom(
461                "couldn't convert nbt to chat message".to_owned(),
462            ))
463        } else {
464            Ok(FormattedText::default())
465        }
466    }
467}
468
469#[cfg(feature = "azalea-buf")]
470#[cfg(feature = "simdnbt")]
471impl AzaleaWrite for FormattedText {
472    fn azalea_write(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
473        let mut out = Vec::new();
474        simdnbt::owned::BaseNbt::write_unnamed(&(self.clone().to_compound().into()), &mut out);
475        buf.write_all(&out)
476    }
477}
478
479impl From<String> for FormattedText {
480    fn from(s: String) -> Self {
481        FormattedText::Text(TextComponent {
482            text: s,
483            base: BaseComponent::default(),
484        })
485    }
486}
487impl From<&str> for FormattedText {
488    fn from(s: &str) -> Self {
489        Self::from(s.to_string())
490    }
491}
492
493impl Display for FormattedText {
494    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
495        match self {
496            FormattedText::Text(c) => c.fmt(f),
497            FormattedText::Translatable(c) => c.fmt(f),
498        }
499    }
500}
501
502impl Default for FormattedText {
503    fn default() -> Self {
504        FormattedText::Text(TextComponent::default())
505    }
506}