azalea_chat/
text_component.rs

1use std::fmt::Display;
2
3use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSerializer};
4
5use crate::{base_component::BaseComponent, style::ChatFormatting, FormattedText};
6
7/// A component that contains text that's the same in all locales.
8#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
9pub struct TextComponent {
10    pub base: BaseComponent,
11    pub text: String,
12}
13
14impl Serialize for TextComponent {
15    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
16    where
17        S: Serializer,
18    {
19        let mut state = serializer.serialize_map(None)?;
20        state.serialize_entry("text", &self.text)?;
21        Serialize::serialize(&self.base, FlatMapSerializer(&mut state))?;
22        if !self.base.siblings.is_empty() {
23            state.serialize_entry("extra", &self.base.siblings)?;
24        }
25        state.end()
26    }
27}
28
29#[cfg(feature = "simdnbt")]
30impl simdnbt::Serialize for TextComponent {
31    fn to_compound(self) -> simdnbt::owned::NbtCompound {
32        let mut compound = simdnbt::owned::NbtCompound::new();
33        compound.insert("text", self.text);
34        compound.extend(self.base.style.to_compound());
35        if !self.base.siblings.is_empty() {
36            compound.insert(
37                "extra",
38                simdnbt::owned::NbtList::from(
39                    self.base
40                        .siblings
41                        .into_iter()
42                        .map(|component| component.to_compound())
43                        .collect::<Vec<_>>(),
44                ),
45            );
46        }
47        compound
48    }
49}
50
51const LEGACY_FORMATTING_CODE_SYMBOL: char = '§';
52
53/// Convert a legacy color code string into a FormattedText
54/// Technically in Minecraft this is done when displaying the text, but AFAIK
55/// it's the same as just doing it in TextComponent
56pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextComponent {
57    let mut components: Vec<TextComponent> = Vec::with_capacity(1);
58    // iterate over legacy_color_code, if it starts with LEGACY_COLOR_CODE_SYMBOL
59    // then read the next character and get the style from that otherwise, add
60    // the character to the text
61
62    // we don't use a normal for loop since we need to be able to skip after reading
63    // the formatter code symbol
64    let mut i = 0;
65    while i < legacy_color_code.chars().count() {
66        if legacy_color_code.chars().nth(i).unwrap() == LEGACY_FORMATTING_CODE_SYMBOL {
67            let formatting_code = legacy_color_code.chars().nth(i + 1);
68            let Some(formatting_code) = formatting_code else {
69                i += 1;
70                continue;
71            };
72            if let Some(formatter) = ChatFormatting::from_code(formatting_code) {
73                if components.is_empty() || !components.last().unwrap().text.is_empty() {
74                    components.push(TextComponent::new("".to_string()));
75                }
76
77                let style = &mut components.last_mut().unwrap().base.style;
78                // if the formatter is a reset, then we need to reset the style to the default
79                style.apply_formatting(&formatter);
80            }
81            i += 1;
82        } else {
83            if components.is_empty() {
84                components.push(TextComponent::new("".to_string()));
85            }
86            components
87                .last_mut()
88                .unwrap()
89                .text
90                .push(legacy_color_code.chars().nth(i).unwrap());
91        };
92        i += 1;
93    }
94
95    if components.is_empty() {
96        return TextComponent::new("".to_string());
97    }
98
99    // create the final component by using the first one as the base, and then
100    // adding the rest as siblings
101    let mut final_component = components.remove(0);
102    for component in components {
103        final_component.base.siblings.push(component.get());
104    }
105
106    final_component
107}
108
109impl TextComponent {
110    pub fn new(text: String) -> Self {
111        // if it contains a LEGACY_FORMATTING_CODE_SYMBOL, format it
112        if text.contains(LEGACY_FORMATTING_CODE_SYMBOL) {
113            legacy_color_code_to_text_component(&text)
114        } else {
115            Self {
116                base: BaseComponent::new(),
117                text,
118            }
119        }
120    }
121
122    fn get(self) -> FormattedText {
123        FormattedText::Text(self)
124    }
125}
126
127impl Display for TextComponent {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        // this contains the final string will all the ansi escape codes
130        for component in FormattedText::Text(self.clone()).into_iter() {
131            let component_text = match &component {
132                FormattedText::Text(c) => c.text.to_string(),
133                FormattedText::Translatable(c) => c.read()?.to_string(),
134            };
135
136            f.write_str(&component_text)?;
137        }
138
139        Ok(())
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::style::Ansi;
147
148    #[test]
149    fn test_hypixel_motd() {
150        let component =
151            TextComponent::new("§aHypixel Network  §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_string())
152                .get();
153        assert_eq!(
154            component.to_ansi(),
155            format!(
156                "{GREEN}Hypixel Network  {RED}[1.8-1.18]\n{BOLD}{AQUA}HAPPY HOLIDAYS{RESET}",
157                GREEN = Ansi::rgb(ChatFormatting::Green.color().unwrap()),
158                RED = Ansi::rgb(ChatFormatting::Red.color().unwrap()),
159                AQUA = Ansi::rgb(ChatFormatting::Aqua.color().unwrap()),
160                BOLD = Ansi::BOLD,
161                RESET = Ansi::RESET
162            )
163        );
164    }
165
166    #[test]
167    fn test_legacy_color_code_to_component() {
168        let component = TextComponent::new("§lHello §r§1w§2o§3r§4l§5d".to_string()).get();
169        assert_eq!(
170            component.to_ansi(),
171            format!(
172                "{BOLD}Hello {RESET}{DARK_BLUE}w{DARK_GREEN}o{DARK_AQUA}r{DARK_RED}l{DARK_PURPLE}d{RESET}",
173                BOLD = Ansi::BOLD,
174                RESET = Ansi::RESET,
175                DARK_BLUE = Ansi::rgb(ChatFormatting::DarkBlue.color().unwrap()),
176                DARK_GREEN = Ansi::rgb(ChatFormatting::DarkGreen.color().unwrap()),
177                DARK_AQUA = Ansi::rgb(ChatFormatting::DarkAqua.color().unwrap()),
178                DARK_RED = Ansi::rgb(ChatFormatting::DarkRed.color().unwrap()),
179                DARK_PURPLE = Ansi::rgb(ChatFormatting::DarkPurple.color().unwrap())
180            )
181        );
182    }
183}