1#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
2use std::io::{self, Cursor, Write};
3use std::{
4 fmt::{self, Display},
5 sync::LazyLock,
6};
7
8#[cfg(feature = "azalea-buf")]
9use azalea_buf::{AzaleaRead, AzaleaWrite, BufReadError};
10use serde::{Deserialize, Deserializer, Serialize, de};
11#[cfg(feature = "simdnbt")]
12use simdnbt::{Deserialize as _, FromNbtTag as _, Serialize as _};
13#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
14use tracing::{debug, trace, warn};
15
16use crate::{
17 base_component::BaseComponent,
18 style::{ChatFormatting, Style},
19 text_component::TextComponent,
20 translatable_component::{StringOrComponent, TranslatableComponent},
21};
22
23#[derive(Clone, Debug, PartialEq, Serialize)]
25#[serde(untagged)]
26pub enum FormattedText {
27 Text(TextComponent),
28 Translatable(TranslatableComponent),
29}
30
31pub static DEFAULT_STYLE: LazyLock<Style> = LazyLock::new(|| Style {
32 color: Some(ChatFormatting::White.try_into().unwrap()),
33 ..Style::default()
34});
35
36impl FormattedText {
38 pub fn get_base_mut(&mut self) -> &mut BaseComponent {
39 match self {
40 Self::Text(c) => &mut c.base,
41 Self::Translatable(c) => &mut c.base,
42 }
43 }
44
45 pub fn get_base(&self) -> &BaseComponent {
46 match self {
47 Self::Text(c) => &c.base,
48 Self::Translatable(c) => &c.base,
49 }
50 }
51
52 fn append(&mut self, sibling: FormattedText) {
54 self.get_base_mut().siblings.push(sibling);
55 }
56
57 fn parse_separator(
59 json: &serde_json::Value,
60 ) -> Result<Option<FormattedText>, serde_json::Error> {
61 if let Some(separator) = json.get("separator") {
62 return Ok(Some(FormattedText::deserialize(separator)?));
63 }
64 Ok(None)
65 }
66
67 #[cfg(feature = "simdnbt")]
68 fn parse_separator_nbt(nbt: &simdnbt::borrow::NbtCompound) -> Option<FormattedText> {
69 if let Some(separator) = nbt.get("separator") {
70 FormattedText::from_nbt_tag(separator)
71 } else {
72 None
73 }
74 }
75
76 pub fn to_custom_format<F, S, C>(
116 &self,
117 mut style_formatter: F,
118 mut text_formatter: S,
119 mut cleanup_formatter: C,
120 default_style: &Style,
121 ) -> String
122 where
123 F: FnMut(&Style, &Style) -> (String, String),
124 S: FnMut(&str) -> String,
125 C: FnMut(&Style) -> String,
126 {
127 let mut output = String::new();
128
129 let mut running_style = Style::default();
130 self.to_custom_format_recursive(
131 &mut output,
132 &mut style_formatter,
133 &mut text_formatter,
134 &default_style.clone(),
135 &mut running_style,
136 );
137 output.push_str(&cleanup_formatter(&running_style));
138
139 output
140 }
141
142 fn to_custom_format_recursive<F, S>(
143 &self,
144 output: &mut String,
145 style_formatter: &mut F,
146 text_formatter: &mut S,
147 parent_style: &Style,
148 running_style: &mut Style,
149 ) where
150 F: FnMut(&Style, &Style) -> (String, String),
151 S: FnMut(&str) -> String,
152 {
153 let component_text = match &self {
154 Self::Text(c) => c.text.to_string(),
155 Self::Translatable(c) => match c.read() {
156 Ok(c) => c.to_string(),
157 Err(_) => c.key.to_string(),
158 },
159 };
160
161 let component_style = &self.get_base().style;
162 let new_style = parent_style.merged_with(component_style);
163
164 if !component_text.is_empty() {
165 let (formatted_style_prefix, formatted_style_suffix) =
166 style_formatter(running_style, &new_style);
167 let formatted_text = text_formatter(&component_text);
168
169 output.push_str(&formatted_style_prefix);
170 output.push_str(&formatted_text);
171 output.push_str(&formatted_style_suffix);
172
173 *running_style = new_style.clone();
174 }
175
176 for sibling in &self.get_base().siblings {
177 sibling.to_custom_format_recursive(
178 output,
179 style_formatter,
180 text_formatter,
181 &new_style,
182 running_style,
183 );
184 }
185 }
186
187 pub fn to_ansi_with_custom_style(&self, default_style: &Style) -> String {
193 self.to_custom_format(
194 |running, new| (running.compare_ansi(new), "".to_owned()),
195 |text| text.to_string(),
196 |style| if !style.is_empty() { "\u{1b}[m" } else { "" }.to_string(),
197 default_style,
198 )
199 }
200
201 pub fn to_ansi(&self) -> String {
226 self.to_ansi_with_custom_style(&DEFAULT_STYLE)
227 }
228
229 pub fn to_html(&self) -> String {
231 self.to_custom_format(
232 |running, new| {
233 (
234 format!(
235 "<span style=\"{}\">",
236 running.merged_with(new).get_html_style()
237 ),
238 "</span>".to_owned(),
239 )
240 },
241 |text| {
242 text.replace("&", "&")
243 .replace("<", "<")
244 .replace(">", ">")
246 .replace("\n", "<br>")
247 },
248 |_| "".to_string(),
249 &DEFAULT_STYLE,
250 )
251 }
252}
253
254impl IntoIterator for FormattedText {
255 type Item = FormattedText;
256 type IntoIter = std::vec::IntoIter<Self::Item>;
257
258 fn into_iter(self) -> Self::IntoIter {
260 let base = self.get_base();
261 let siblings = base.siblings.clone();
262 let mut v: Vec<FormattedText> = Vec::with_capacity(siblings.len() + 1);
263 v.push(self);
264 for sibling in siblings {
265 v.extend(sibling);
266 }
267
268 v.into_iter()
269 }
270}
271
272impl<'de> Deserialize<'de> for FormattedText {
273 fn deserialize<D>(de: D) -> Result<Self, D::Error>
274 where
275 D: Deserializer<'de>,
276 {
277 let json: serde_json::Value = serde::Deserialize::deserialize(de)?;
278
279 let mut component: FormattedText;
281
282 if !json.is_array() && !json.is_object() {
284 return Ok(FormattedText::Text(TextComponent::new(
285 json.as_str().unwrap_or("").to_string(),
286 )));
287 }
288 else if json.is_object() {
290 if let Some(text) = json.get("text") {
291 let text = text.as_str().unwrap_or("").to_string();
292 component = FormattedText::Text(TextComponent::new(text));
293 } else if let Some(translate) = json.get("translate") {
294 let translate = translate
295 .as_str()
296 .ok_or_else(|| de::Error::custom("\"translate\" must be a string"))?
297 .into();
298 let fallback = if let Some(fallback) = json.get("fallback") {
299 Some(
300 fallback
301 .as_str()
302 .ok_or_else(|| de::Error::custom("\"fallback\" must be a string"))?
303 .to_string(),
304 )
305 } else {
306 None
307 };
308 if let Some(with) = json.get("with") {
309 let with = with
310 .as_array()
311 .ok_or_else(|| de::Error::custom("\"with\" must be an array"))?;
312 let mut with_array = Vec::with_capacity(with.len());
313 for item in with {
314 let c = FormattedText::deserialize(item).map_err(de::Error::custom)?;
318 if let FormattedText::Text(text_component) = c
319 && text_component.base.siblings.is_empty()
320 && text_component.base.style.is_empty()
321 {
322 with_array.push(StringOrComponent::String(text_component.text));
323 continue;
324 }
325 with_array.push(StringOrComponent::FormattedText(
326 FormattedText::deserialize(item).map_err(de::Error::custom)?,
327 ));
328 }
329 component = FormattedText::Translatable(TranslatableComponent::with_fallback(
330 translate, fallback, with_array,
331 ));
332 } else {
333 component = FormattedText::Translatable(TranslatableComponent::with_fallback(
335 translate,
336 fallback,
337 Vec::new(),
338 ));
339 }
340 } else if let Some(score) = json.get("score") {
341 if score.get("name").is_none() || score.get("objective").is_none() {
343 return Err(de::Error::missing_field(
344 "A score component needs at least a name and an objective",
345 ));
346 }
347 return Err(de::Error::custom(
349 "score text components aren't yet supported",
350 ));
351 } else if json.get("selector").is_some() {
352 return Err(de::Error::custom(
353 "selector text components aren't yet supported",
354 ));
355 } else if json.get("keybind").is_some() {
356 return Err(de::Error::custom(
357 "keybind text components aren't yet supported",
358 ));
359 } else {
360 let Some(_nbt) = json.get("nbt") else {
361 return Err(de::Error::custom(
362 format!("Don't know how to turn {json} into a FormattedText").as_str(),
363 ));
364 };
365 let _separator =
366 FormattedText::parse_separator(&json).map_err(de::Error::custom)?;
367
368 let _interpret = match json.get("interpret") {
369 Some(v) => v.as_bool().ok_or(Some(false)).unwrap(),
370 None => false,
371 };
372 if let Some(_block) = json.get("block") {}
373 return Err(de::Error::custom(
374 "nbt text components aren't yet supported",
375 ));
376 }
377 if let Some(extra) = json.get("extra") {
378 let Some(extra) = extra.as_array() else {
379 return Err(de::Error::custom("Extra isn't an array"));
380 };
381 if extra.is_empty() {
382 return Err(de::Error::custom("Unexpected empty array of components"));
383 }
384 for extra_component in extra {
385 let sibling =
386 FormattedText::deserialize(extra_component).map_err(de::Error::custom)?;
387 component.append(sibling);
388 }
389 }
390
391 let style = Style::deserialize(&json);
392 component.get_base_mut().style = Box::new(style);
393
394 return Ok(component);
395 }
396 else if !json.is_array() {
398 return Err(de::Error::custom(
399 format!("Don't know how to turn {json} into a FormattedText").as_str(),
400 ));
401 }
402 let json_array = json.as_array().unwrap();
403 let mut component =
406 FormattedText::deserialize(&json_array[0]).map_err(de::Error::custom)?;
407 for i in 1..json_array.len() {
408 component.append(
409 FormattedText::deserialize(json_array.get(i).unwrap())
410 .map_err(de::Error::custom)?,
411 );
412 }
413 Ok(component)
414 }
415}
416
417#[cfg(feature = "simdnbt")]
418impl simdnbt::Serialize for FormattedText {
419 fn to_compound(self) -> simdnbt::owned::NbtCompound {
420 match self {
421 FormattedText::Text(c) => c.to_compound(),
422 FormattedText::Translatable(c) => c.to_compound(),
423 }
424 }
425}
426
427#[cfg(feature = "simdnbt")]
428impl simdnbt::FromNbtTag for FormattedText {
429 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
430 if let Some(string) = tag.string() {
432 Some(FormattedText::from_nbt_string(string))
433 }
434 else if let Some(compound) = tag.compound() {
437 FormattedText::from_nbt_compound(compound)
438 }
439 else if let Some(list) = tag.list() {
441 FormattedText::from_nbt_list(list)
442 } else {
443 Some(FormattedText::Text(TextComponent::new("".to_owned())))
444 }
445 }
446}
447
448#[cfg(feature = "simdnbt")]
449impl FormattedText {
450 fn from_nbt_string(s: &simdnbt::Mutf8Str) -> Self {
451 FormattedText::from(s)
452 }
453 fn from_nbt_list(list: simdnbt::borrow::NbtList) -> Option<FormattedText> {
454 let mut component;
455 if let Some(compounds) = list.compounds() {
456 component = FormattedText::from_nbt_compound(compounds.first()?)?;
457 for compound in compounds.into_iter().skip(1) {
458 component.append(FormattedText::from_nbt_compound(compound)?);
459 }
460 } else if let Some(strings) = list.strings() {
461 component = FormattedText::from(*(strings.first()?));
462 for &string in strings.iter().skip(1) {
463 component.append(FormattedText::from(string));
464 }
465 } else {
466 debug!("couldn't parse {list:?} as FormattedText");
467 return None;
468 }
469 Some(component)
470 }
471
472 pub fn from_nbt_compound(compound: simdnbt::borrow::NbtCompound) -> Option<Self> {
473 let mut component: FormattedText;
474
475 if let Some(text) = compound.get("text") {
476 let text = text.string().unwrap_or_default().to_string();
477 component = FormattedText::Text(TextComponent::new(text));
478 } else if let Some(translate) = compound.get("translate") {
479 let translate = translate.string()?.into();
480 if let Some(with) = compound.get("with") {
481 let mut with_array = Vec::new();
482 let with_list = with.list()?;
483 if with_list.empty() {
484 } else if let Some(with) = with_list.strings() {
485 for item in with {
486 with_array.push(StringOrComponent::String(item.to_string()));
487 }
488 } else if let Some(with) = with_list.ints() {
489 for item in with {
490 with_array.push(StringOrComponent::String(item.to_string()));
491 }
492 } else if let Some(with) = with_list.compounds() {
493 for item in with {
494 if let Some(primitive) = item.get("") {
499 if let Some(b) = primitive.byte() {
502 with_array.push(StringOrComponent::String(
504 if b != 0 { "true" } else { "false" }.to_string(),
505 ));
506 } else if let Some(s) = primitive.short() {
507 with_array.push(StringOrComponent::String(s.to_string()));
508 } else if let Some(i) = primitive.int() {
509 with_array.push(StringOrComponent::String(i.to_string()));
510 } else if let Some(l) = primitive.long() {
511 with_array.push(StringOrComponent::String(l.to_string()));
512 } else if let Some(f) = primitive.float() {
513 with_array.push(StringOrComponent::String(f.to_string()));
514 } else if let Some(d) = primitive.double() {
515 with_array.push(StringOrComponent::String(d.to_string()));
516 } else if let Some(s) = primitive.string() {
517 with_array.push(StringOrComponent::String(s.to_string()));
518 } else {
519 warn!(
520 "couldn't parse {item:?} as FormattedText because it has a disallowed primitive"
521 );
522 with_array.push(StringOrComponent::String("?".to_string()));
523 }
524 } else if let Some(c) = FormattedText::from_nbt_compound(item) {
525 if let FormattedText::Text(text_component) = c
526 && text_component.base.siblings.is_empty()
527 && text_component.base.style.is_empty()
528 {
529 with_array.push(StringOrComponent::String(text_component.text));
530 continue;
531 }
532 with_array.push(StringOrComponent::FormattedText(
533 FormattedText::from_nbt_compound(item)?,
534 ));
535 } else {
536 warn!("couldn't parse {item:?} as FormattedText");
537 with_array.push(StringOrComponent::String("?".to_string()));
538 }
539 }
540 } else {
541 warn!(
542 "couldn't parse {with:?} as FormattedText because it's not a list of compounds"
543 );
544 return None;
545 }
546 component =
547 FormattedText::Translatable(TranslatableComponent::new(translate, with_array));
548 } else {
549 component =
551 FormattedText::Translatable(TranslatableComponent::new(translate, Vec::new()));
552 }
553 } else if let Some(score) = compound.compound("score") {
554 if score.get("name").is_none() || score.get("objective").is_none() {
556 trace!("A score component needs at least a name and an objective");
558 return None;
559 }
560 return None;
562 } else if compound.get("selector").is_some() {
563 trace!("selector text components aren't yet supported");
565 return None;
566 } else if compound.get("keybind").is_some() {
567 trace!("keybind text components aren't yet supported");
569 return None;
570 } else if let Some(tag) = compound.get("") {
571 return FormattedText::from_nbt_tag(tag);
572 } else {
573 let _nbt = compound.get("nbt")?;
574 let _separator = FormattedText::parse_separator_nbt(&compound)?;
575
576 let _interpret = match compound.get("interpret") {
577 Some(v) => v.byte().unwrap_or_default() != 0,
578 None => false,
579 };
580 if let Some(_block) = compound.get("block") {}
581 trace!("nbt text components aren't yet supported");
582 return None;
583 }
584 if let Some(extra) = compound.get("extra") {
585 if let Some(items) = extra.list() {
587 if let Some(items) = items.compounds() {
588 for item in items {
589 component.append(FormattedText::from_nbt_compound(item)?);
590 }
591 } else if let Some(items) = items.strings() {
592 for item in items {
593 component.append(FormattedText::from_nbt_string(item));
594 }
595 } else if let Some(items) = items.lists() {
596 for item in items {
597 component.append(FormattedText::from_nbt_list(item)?);
598 }
599 } else {
600 warn!(
601 "couldn't parse {items:?} as FormattedText because it's not a list of compounds or strings"
602 );
603 }
604 } else {
605 component.append(FormattedText::from_nbt_tag(extra)?);
606 }
607 }
608
609 let base_style = Style::from_compound(compound).ok()?;
610 let new_style = &mut component.get_base_mut().style;
611 *new_style = Box::new(new_style.merged_with(&base_style));
612
613 Some(component)
614 }
615}
616
617#[cfg(feature = "simdnbt")]
618impl From<&simdnbt::Mutf8Str> for FormattedText {
619 fn from(s: &simdnbt::Mutf8Str) -> Self {
620 FormattedText::Text(TextComponent::new(s.to_string()))
621 }
622}
623
624#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
625impl AzaleaRead for FormattedText {
626 fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
627 let nbt = simdnbt::borrow::read_optional_tag(buf)?;
628 trace!(
629 "Reading NBT for FormattedText: {:?}",
630 nbt.as_ref().map(|n| n.as_tag().to_owned())
631 );
632 match nbt {
633 Some(nbt) => FormattedText::from_nbt_tag(nbt.as_tag()).ok_or(BufReadError::Custom(
634 "couldn't convert nbt to chat message".to_owned(),
635 )),
636 _ => Ok(FormattedText::default()),
637 }
638 }
639}
640
641#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
642impl AzaleaWrite for FormattedText {
643 fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
644 let mut out = Vec::new();
645 simdnbt::owned::BaseNbt::write_unnamed(&(self.clone().to_compound().into()), &mut out);
646 buf.write_all(&out)
647 }
648}
649
650impl From<String> for FormattedText {
651 fn from(s: String) -> Self {
652 FormattedText::Text(TextComponent {
653 text: s,
654 base: BaseComponent::default(),
655 })
656 }
657}
658impl From<&str> for FormattedText {
659 fn from(s: &str) -> Self {
660 Self::from(s.to_string())
661 }
662}
663impl From<TranslatableComponent> for FormattedText {
664 fn from(c: TranslatableComponent) -> Self {
665 FormattedText::Translatable(c)
666 }
667}
668impl From<TextComponent> for FormattedText {
669 fn from(c: TextComponent) -> Self {
670 FormattedText::Text(c)
671 }
672}
673
674impl Display for FormattedText {
675 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
680 match self {
681 FormattedText::Text(c) => c.fmt(f),
682 FormattedText::Translatable(c) => c.fmt(f),
683 }
684 }
685}
686
687impl Default for FormattedText {
688 fn default() -> Self {
689 FormattedText::Text(TextComponent::default())
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 use serde_json::Value;
696
697 use super::*;
698
699 #[test]
700 fn deserialize_translation() {
701 let j: Value =
702 serde_json::from_str(r#"{"translate": "translation.test.args", "with": ["a", "b"]}"#)
703 .unwrap();
704 let component = FormattedText::deserialize(&j).unwrap();
705 assert_eq!(
706 component,
707 FormattedText::Translatable(TranslatableComponent::new(
708 "translation.test.args".to_string(),
709 vec![
710 StringOrComponent::String("a".to_string()),
711 StringOrComponent::String("b".to_string())
712 ]
713 ))
714 );
715 }
716
717 #[test]
718 fn deserialize_translation_invalid_arguments() {
719 let j: Value =
720 serde_json::from_str(r#"{"translate": "translation.test.args", "with": {}}"#).unwrap();
721 assert!(FormattedText::deserialize(&j).is_err());
722 }
723
724 #[test]
725 fn deserialize_translation_fallback() {
726 let j: Value = serde_json::from_str(r#"{"translate": "translation.test.undefined", "fallback": "fallback: %s", "with": ["a"]}"#).unwrap();
727 let component = FormattedText::deserialize(&j).unwrap();
728 assert_eq!(
729 component,
730 FormattedText::Translatable(TranslatableComponent::with_fallback(
731 "translation.test.undefined".to_string(),
732 Some("fallback: %s".to_string()),
733 vec![StringOrComponent::String("a".to_string())]
734 ))
735 );
736 }
737
738 #[test]
739 fn deserialize_translation_invalid_fallback() {
740 let j: Value = serde_json::from_str(
741 r#"{"translate": "translation.test.undefined", "fallback": {"text": "invalid"}}"#,
742 )
743 .unwrap();
744 assert!(FormattedText::deserialize(&j).is_err());
745 }
746}