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#[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
30impl 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 fn append(&mut self, sibling: FormattedText) {
48 self.get_base_mut().siblings.push(sibling);
49 }
50
51 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 pub fn to_ansi(&self) -> String {
92 self.to_ansi_with_custom_style(&DEFAULT_STYLE)
94 }
95
96 pub fn to_ansi_with_custom_style(&self, default_style: &Style) -> String {
102 let mut built_string = String::new();
104 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 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 let mut component: FormattedText;
157
158 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 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 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 component = FormattedText::Translatable(TranslatableComponent::new(
202 translate,
203 Vec::new(),
204 ));
205 }
206 } else if let Some(score) = json.get("score") {
207 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 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 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 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 let Some(string) = tag.string() {
298 Some(FormattedText::from(string))
299 }
300 else if let Some(compound) = tag.compound() {
303 FormattedText::from_nbt_compound(compound)
304 }
305 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 let Some(primitive) = item.get("") {
354 if let Some(b) = primitive.byte() {
357 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 component =
403 FormattedText::Translatable(TranslatableComponent::new(translate, Vec::new()));
404 }
405 } else if let Some(score) = compound.compound("score") {
406 if score.get("name").is_none() || score.get("objective").is_none() {
408 trace!("A score component needs at least a name and an objective");
410 return None;
411 }
412 return None;
414 } else if compound.get("selector").is_some() {
415 trace!("selector text components aren't yet supported");
417 return None;
418 } else if compound.get("keybind").is_some() {
419 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}