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