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::{AzBuf, 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_owned(),
211 |style| if !style.is_empty() { "\u{1b}[m" } else { "" }.to_owned(),
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_owned(),
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_owned(),
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_owned();
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_owned(),
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 if json_array.is_empty() {
412 return Ok(FormattedText::default());
413 }
414 let mut component =
417 FormattedText::deserialize(&json_array[0]).map_err(de::Error::custom)?;
418 for i in 1..json_array.len() {
419 component.append(
420 FormattedText::deserialize(json_array.get(i).unwrap())
421 .map_err(de::Error::custom)?,
422 );
423 }
424 Ok(component)
425 }
426}
427
428#[cfg(feature = "simdnbt")]
429impl simdnbt::Serialize for FormattedText {
430 fn to_compound(self) -> simdnbt::owned::NbtCompound {
431 match self {
432 FormattedText::Text(c) => c.to_compound(),
433 FormattedText::Translatable(c) => c.to_compound(),
434 }
435 }
436}
437
438#[cfg(feature = "simdnbt")]
439impl simdnbt::FromNbtTag for FormattedText {
440 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
441 if let Some(string) = tag.string() {
443 Some(FormattedText::from_nbt_string(string))
444 }
445 else if let Some(compound) = tag.compound() {
448 FormattedText::from_nbt_compound(compound)
449 }
450 else if let Some(list) = tag.list() {
452 FormattedText::from_nbt_list(list)
453 } else {
454 Some(FormattedText::Text(TextComponent::new("".to_owned())))
455 }
456 }
457}
458
459#[cfg(feature = "simdnbt")]
460impl FormattedText {
461 fn from_nbt_string(s: &simdnbt::Mutf8Str) -> Self {
462 FormattedText::from(s)
463 }
464 fn from_nbt_list(list: simdnbt::borrow::NbtList) -> Option<FormattedText> {
465 use tracing::debug;
466
467 let mut component;
468 if let Some(compounds) = list.compounds() {
469 component = FormattedText::from_nbt_compound(compounds.first()?)?;
470 for compound in compounds.into_iter().skip(1) {
471 component.append(FormattedText::from_nbt_compound(compound)?);
472 }
473 } else if let Some(strings) = list.strings() {
474 component = FormattedText::from(*(strings.first()?));
475 for &string in strings.iter().skip(1) {
476 component.append(FormattedText::from(string));
477 }
478 } else {
479 debug!("couldn't parse {list:?} as FormattedText");
480 return None;
481 }
482 Some(component)
483 }
484
485 pub fn from_nbt_compound(compound: simdnbt::borrow::NbtCompound) -> Option<Self> {
486 use simdnbt::{Deserialize, FromNbtTag};
487 use tracing::{trace, warn};
488
489 let mut component: FormattedText;
490
491 if let Some(text) = compound.get("text") {
492 let text = text.string().unwrap_or_default().to_string();
493 component = FormattedText::Text(TextComponent::new(text));
494 } else if let Some(translate) = compound.get("translate") {
495 let translate = translate.string()?.into();
496 if let Some(with) = compound.get("with") {
497 let mut with_array = Vec::new();
498 let with_list = with.list()?;
499 if with_list.empty() {
500 } else if let Some(with) = with_list.strings() {
501 for item in with {
502 with_array.push(PrimitiveOrComponent::String(item.to_string()));
503 }
504 } else if let Some(with) = with_list.ints() {
505 for item in with {
506 with_array.push(PrimitiveOrComponent::Integer(item));
507 }
508 } else if let Some(with) = with_list.longs() {
509 for item in with {
510 with_array.push(PrimitiveOrComponent::Long(item));
511 }
512 } else if let Some(with) = with_list.compounds() {
513 for item in with {
514 if let Some(primitive) = item.get("") {
519 if let Some(b) = primitive.byte() {
522 with_array.push(PrimitiveOrComponent::Boolean(b != 0));
524 } else if let Some(s) = primitive.short() {
525 with_array.push(PrimitiveOrComponent::Short(s));
526 } else if let Some(i) = primitive.int() {
527 with_array.push(PrimitiveOrComponent::Integer(i));
528 } else if let Some(l) = primitive.long() {
529 with_array.push(PrimitiveOrComponent::Long(l));
530 } else if let Some(f) = primitive.float() {
531 with_array.push(PrimitiveOrComponent::Float(f));
532 } else if let Some(d) = primitive.double() {
533 with_array.push(PrimitiveOrComponent::Double(d));
534 } else if let Some(s) = primitive.string() {
535 with_array.push(PrimitiveOrComponent::String(s.to_string()));
536 } else {
537 warn!(
538 "couldn't parse {item:?} as FormattedText because it has a disallowed primitive"
539 );
540 with_array.push(PrimitiveOrComponent::String("?".to_owned()));
541 }
542 } else if let Some(c) = FormattedText::from_nbt_compound(item) {
543 if let FormattedText::Text(text_component) = c
544 && text_component.base.siblings.is_empty()
545 && text_component.base.style.is_empty()
546 {
547 with_array.push(PrimitiveOrComponent::String(text_component.text));
548 continue;
549 }
550 with_array.push(PrimitiveOrComponent::FormattedText(
551 FormattedText::from_nbt_compound(item)?,
552 ));
553 } else {
554 warn!("couldn't parse {item:?} as FormattedText");
555 with_array.push(PrimitiveOrComponent::String("?".to_owned()));
556 }
557 }
558 } else {
559 warn!(
560 "couldn't parse {with:?} as FormattedText because it's not a list of compounds"
561 );
562 return None;
563 }
564 component =
565 FormattedText::Translatable(TranslatableComponent::new(translate, with_array));
566 } else {
567 component =
569 FormattedText::Translatable(TranslatableComponent::new(translate, Vec::new()));
570 }
571 } else if let Some(score) = compound.compound("score") {
572 if score.get("name").is_none() || score.get("objective").is_none() {
573 trace!("A score component needs at least a name and an objective");
574 return None;
575 }
576 return None;
578 } else if compound.get("selector").is_some() {
579 trace!("selector text components aren't supported");
580 return None;
581 } else if compound.get("keybind").is_some() {
582 trace!("keybind text components aren't supported");
583 return None;
584 } else if compound.get("object").is_some() {
585 trace!("object text components aren't supported");
586 return None;
587 } else if let Some(tag) = compound.get("") {
588 return FormattedText::from_nbt_tag(tag);
589 } else {
590 let _nbt = compound.get("nbt")?;
591 let _separator = FormattedText::parse_separator_nbt(&compound)?;
592
593 let _interpret = match compound.get("interpret") {
594 Some(v) => v.byte().unwrap_or_default() != 0,
595 None => false,
596 };
597 if let Some(_block) = compound.get("block") {}
598 trace!("nbt text components aren't yet supported");
599 return None;
600 }
601 if let Some(extra) = compound.get("extra") {
602 if let Some(items) = extra.list() {
604 if let Some(items) = items.compounds() {
605 for item in items {
606 component.append(FormattedText::from_nbt_compound(item)?);
607 }
608 } else if let Some(items) = items.strings() {
609 for item in items {
610 component.append(FormattedText::from_nbt_string(item));
611 }
612 } else if let Some(items) = items.lists() {
613 for item in items {
614 component.append(FormattedText::from_nbt_list(item)?);
615 }
616 } else {
617 warn!(
618 "couldn't parse {items:?} as FormattedText because it's not a list of compounds or strings"
619 );
620 }
621 } else {
622 component.append(FormattedText::from_nbt_tag(extra)?);
623 }
624 }
625
626 let base_style = Style::from_compound(compound).ok()?;
627 let new_style = &mut component.get_base_mut().style;
628 **new_style = new_style.merged_with(&base_style);
629
630 Some(component)
631 }
632}
633
634#[cfg(feature = "simdnbt")]
635impl From<&simdnbt::Mutf8Str> for FormattedText {
636 fn from(s: &simdnbt::Mutf8Str) -> Self {
637 FormattedText::Text(TextComponent::new(s.to_string()))
638 }
639}
640
641#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
642impl AzBuf for FormattedText {
643 fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
644 use simdnbt::FromNbtTag;
645 use tracing::trace;
646
647 let nbt = simdnbt::borrow::read_optional_tag(buf)?;
648 trace!(
649 "Reading NBT for FormattedText: {:?}",
650 nbt.as_ref().map(|n| n.as_tag().to_owned())
651 );
652 match nbt {
653 Some(nbt) => FormattedText::from_nbt_tag(nbt.as_tag()).ok_or(BufReadError::Custom(
654 "couldn't convert nbt to chat message".to_owned(),
655 )),
656 _ => Ok(FormattedText::default()),
657 }
658 }
659 fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
660 use simdnbt::Serialize;
661
662 let mut out = Vec::new();
663 simdnbt::owned::BaseNbt::write_unnamed(&(self.clone().to_compound().into()), &mut out);
664 buf.write_all(&out)
665 }
666}
667
668impl From<String> for FormattedText {
669 fn from(s: String) -> Self {
670 FormattedText::Text(TextComponent {
671 text: s,
672 base: BaseComponent::default(),
673 })
674 }
675}
676impl From<&str> for FormattedText {
677 fn from(s: &str) -> Self {
678 Self::from(s.to_owned())
679 }
680}
681
682impl Display for FormattedText {
683 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
688 match self {
689 FormattedText::Text(c) => c.fmt(f),
690 FormattedText::Translatable(c) => c.fmt(f),
691 }
692 }
693}
694
695impl Default for FormattedText {
696 fn default() -> Self {
697 FormattedText::Text(TextComponent::default())
698 }
699}
700
701#[cfg(test)]
702mod tests {
703 use serde_json::Value;
704
705 use super::*;
706 use crate::style::TextColor;
707
708 #[test]
709 fn deserialize_translation() {
710 let j: Value =
711 serde_json::from_str(r#"{"translate": "translation.test.args", "with": ["a", "b"]}"#)
712 .unwrap();
713 let component = FormattedText::deserialize(&j).unwrap();
714 assert_eq!(
715 component,
716 FormattedText::Translatable(TranslatableComponent::new(
717 "translation.test.args".to_owned(),
718 vec![
719 PrimitiveOrComponent::String("a".to_owned()),
720 PrimitiveOrComponent::String("b".to_owned())
721 ]
722 ))
723 );
724 }
725
726 #[test]
727 fn deserialize_translation_invalid_arguments() {
728 let j: Value =
729 serde_json::from_str(r#"{"translate": "translation.test.args", "with": {}}"#).unwrap();
730 assert!(FormattedText::deserialize(&j).is_err());
731 }
732
733 #[test]
734 fn deserialize_translation_fallback() {
735 let j: Value = serde_json::from_str(r#"{"translate": "translation.test.undefined", "fallback": "fallback: %s", "with": ["a"]}"#).unwrap();
736 let component = FormattedText::deserialize(&j).unwrap();
737 assert_eq!(
738 component,
739 FormattedText::Translatable(TranslatableComponent::with_fallback(
740 "translation.test.undefined".to_owned(),
741 Some("fallback: %s".to_owned()),
742 vec![PrimitiveOrComponent::String("a".to_owned())]
743 ))
744 );
745 }
746
747 #[test]
748 fn deserialize_translation_invalid_fallback() {
749 let j: Value = serde_json::from_str(
750 r#"{"translate": "translation.test.undefined", "fallback": {"text": "invalid"}}"#,
751 )
752 .unwrap();
753 assert!(FormattedText::deserialize(&j).is_err());
754 }
755 #[test]
756 fn deserialize_translation_primitive_args() {
757 let j: Value = serde_json::from_str(
758 r#"{"translate":"commands.list.players", "with": [1, 65536, "<players>", {"text": "unused", "color": "red"}]}"#,
759 )
760 .unwrap();
761 assert_eq!(
762 FormattedText::deserialize(&j).unwrap(),
763 FormattedText::Translatable(TranslatableComponent::new(
764 "commands.list.players".to_owned(),
765 vec![
766 PrimitiveOrComponent::Short(1),
767 PrimitiveOrComponent::Integer(65536),
768 PrimitiveOrComponent::String("<players>".to_owned()),
769 PrimitiveOrComponent::FormattedText(FormattedText::Text(
770 TextComponent::new("unused")
771 .with_style(Style::new().color(Some(TextColor::parse("red").unwrap())))
772 ))
773 ]
774 ))
775 );
776 }
777
778 #[test]
779 fn test_translatable_with_color_inheritance() {
780 let json = serde_json::json!({
781 "translate": "advancements.story.smelt_iron.title",
782 "color": "green",
783 "with": [{"text": "Acquire Hardware"}]
784 });
785 let component = FormattedText::deserialize(&json).unwrap();
786 let ansi = component.to_ansi();
787 assert!(ansi.contains("\u{1b}[38;2;85;255;85m"));
788 }
789}