azalea_chat/
text_component.rs1use std::fmt::{self, Display};
2
3use serde::{__private::ser::FlatMapSerializer, Serialize, Serializer, ser::SerializeMap};
4
5use crate::{
6 FormattedText,
7 base_component::BaseComponent,
8 style::{ChatFormatting, Style, TextColor},
9};
10
11#[derive(Clone, Debug, Default, PartialEq)]
13pub struct TextComponent {
14 pub base: BaseComponent,
15 pub text: String,
16}
17impl Serialize for TextComponent {
18 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
19 where
20 S: Serializer,
21 {
22 if self.base == BaseComponent::default() {
23 return serializer.serialize_str(&self.text);
24 }
25
26 let mut state = serializer.serialize_map(None)?;
27 state.serialize_entry("text", &self.text)?;
28 Serialize::serialize(&self.base, FlatMapSerializer(&mut state))?;
29 if !self.base.siblings.is_empty() {
30 state.serialize_entry("extra", &self.base.siblings)?;
31 }
32 state.end()
33 }
34}
35
36#[cfg(feature = "simdnbt")]
37impl simdnbt::Serialize for TextComponent {
38 fn to_compound(self) -> simdnbt::owned::NbtCompound {
39 let mut compound = simdnbt::owned::NbtCompound::new();
40 compound.insert("text", self.text);
41 compound.extend(self.base.style.to_compound());
42 if !self.base.siblings.is_empty() {
43 compound.insert(
44 "extra",
45 simdnbt::owned::NbtList::from(
46 self.base
47 .siblings
48 .into_iter()
49 .map(|component| component.to_compound())
50 .collect::<Vec<_>>(),
51 ),
52 );
53 }
54 compound
55 }
56}
57
58const LEGACY_FORMATTING_CODE_SYMBOL: char = '§';
59
60pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextComponent {
64 if legacy_color_code.is_empty() {
65 return TextComponent::new("");
66 }
67
68 let mut components: Vec<TextComponent> = Vec::with_capacity(1);
69 let mut cur_component = TextComponent::new("");
74
75 let mut i = 0;
78 while i < legacy_color_code.chars().count() {
79 if legacy_color_code.chars().nth(i).unwrap() == LEGACY_FORMATTING_CODE_SYMBOL {
80 let formatting_code = legacy_color_code.chars().nth(i + 1);
81 let Some(formatting_code) = formatting_code else {
82 i += 1;
83 continue;
84 };
85 if formatting_code == '#' {
86 let color = legacy_color_code
87 .chars()
88 .skip(i + 1)
89 .take(7)
91 .collect::<String>();
92
93 if !cur_component.text.is_empty() {
94 components.push(cur_component.clone());
96 cur_component.text = "".to_string();
97 };
98 cur_component.base.style.color = TextColor::parse(&color);
99
100 i += 6;
101 } else if let Some(formatter) = ChatFormatting::from_code(formatting_code) {
102 if !cur_component.text.is_empty() || formatter == ChatFormatting::Reset {
103 components.push(cur_component.clone());
105 cur_component.text = "".to_string();
106 };
107 cur_component.base.style.apply_formatting(&formatter);
108 }
109 i += 1;
110 } else {
111 cur_component
112 .text
113 .push(legacy_color_code.chars().nth(i).unwrap());
114 };
115 i += 1;
116 }
117
118 components.push(cur_component);
119
120 let mut final_component = TextComponent::new("");
122 for component in components {
123 final_component.base.siblings.push(component.get());
124 }
125
126 final_component
127}
128
129impl TextComponent {
130 pub fn new(text: impl Into<String>) -> Self {
131 let text = text.into();
132 if text.contains(LEGACY_FORMATTING_CODE_SYMBOL) {
134 legacy_color_code_to_text_component(&text)
135 } else {
136 Self {
137 base: BaseComponent::new(),
138 text,
139 }
140 }
141 }
142
143 fn get(self) -> FormattedText {
144 FormattedText::Text(self)
145 }
146 pub fn with_style(mut self, style: Style) -> Self {
147 self.base.style = Box::new(style);
148 self
149 }
150}
151
152impl Display for TextComponent {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 for component in FormattedText::Text(self.clone()).into_iter() {
156 let component_text = match &component {
157 FormattedText::Text(c) => c.text.to_string(),
158 FormattedText::Translatable(c) => c.read()?.to_string(),
159 };
160
161 f.write_str(&component_text)?;
162 }
163
164 Ok(())
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::style::Ansi;
172
173 #[test]
174 fn test_hypixel_motd_ansi() {
175 let component =
176 TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_string())
177 .get();
178 assert_eq!(
179 component.to_ansi(),
180 format!(
181 "{GREEN}Hypixel Network {RED}[1.8-1.18]\n{BOLD}{AQUA}HAPPY HOLIDAYS{RESET}",
182 GREEN = Ansi::rgb(ChatFormatting::Green.color().unwrap()),
183 RED = Ansi::rgb(ChatFormatting::Red.color().unwrap()),
184 AQUA = Ansi::rgb(ChatFormatting::Aqua.color().unwrap()),
185 BOLD = Ansi::BOLD,
186 RESET = Ansi::RESET
187 )
188 );
189 }
190
191 #[test]
192 fn test_hypixel_motd_html() {
193 let component =
194 TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_string())
195 .get();
196
197 assert_eq!(
198 component.to_html(),
199 format!(
200 "{GREEN}Hypixel Network {END_SPAN}{RED}[1.8-1.18]<br>{END_SPAN}{BOLD_AQUA}HAPPY HOLIDAYS{END_SPAN}",
201 END_SPAN = "</span>",
202 GREEN = "<span style=\"color:#55FF55;\">",
203 RED = "<span style=\"color:#FF5555;\">",
204 BOLD_AQUA = "<span style=\"color:#55FFFF;font-weight:bold;\">",
205 )
206 );
207 }
208
209 #[test]
210 fn test_xss_html() {
211 let component = TextComponent::new("§a<b>&\n§b</b>".to_string()).get();
212
213 assert_eq!(
214 component.to_html(),
215 format!(
216 "{GREEN}<b>&<br>{END_SPAN}{AQUA}</b>{END_SPAN}",
217 END_SPAN = "</span>",
218 GREEN = "<span style=\"color:#55FF55;\">",
219 AQUA = "<span style=\"color:#55FFFF;\">",
220 )
221 );
222 }
223
224 #[test]
225 fn test_legacy_color_code_to_component() {
226 let component = TextComponent::new("§lHello §r§1w§2o§3r§4l§5d".to_string()).get();
227 assert_eq!(
228 component.to_ansi(),
229 format!(
230 "{BOLD}{WHITE}Hello {RESET}{DARK_BLUE}w{DARK_GREEN}o{DARK_AQUA}r{DARK_RED}l{DARK_PURPLE}d{RESET}",
231 BOLD = Ansi::BOLD,
232 WHITE = Ansi::rgb(ChatFormatting::White.color().unwrap()),
233 RESET = Ansi::RESET,
234 DARK_BLUE = Ansi::rgb(ChatFormatting::DarkBlue.color().unwrap()),
235 DARK_GREEN = Ansi::rgb(ChatFormatting::DarkGreen.color().unwrap()),
236 DARK_AQUA = Ansi::rgb(ChatFormatting::DarkAqua.color().unwrap()),
237 DARK_RED = Ansi::rgb(ChatFormatting::DarkRed.color().unwrap()),
238 DARK_PURPLE = Ansi::rgb(ChatFormatting::DarkPurple.color().unwrap())
239 )
240 );
241 }
242
243 #[test]
244 fn test_legacy_color_code_with_rgb() {
245 let component = TextComponent::new("§#Ff0000This is a test message".to_string()).get();
246 assert_eq!(
247 component.to_ansi(),
248 format!(
249 "{RED}This is a test message{RESET}",
250 RED = Ansi::rgb(0xff0000),
251 RESET = Ansi::RESET
252 )
253 );
254 }
255}