azalea_chat/
component.rs

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