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