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.compounds() {
509 for item in with {
510 if let Some(primitive) = item.get("") {
515 if let Some(b) = primitive.byte() {
518 with_array.push(PrimitiveOrComponent::Boolean(b != 0));
520 } else if let Some(s) = primitive.short() {
521 with_array.push(PrimitiveOrComponent::Short(s));
522 } else if let Some(i) = primitive.int() {
523 with_array.push(PrimitiveOrComponent::Integer(i));
524 } else if let Some(l) = primitive.long() {
525 with_array.push(PrimitiveOrComponent::Long(l));
526 } else if let Some(f) = primitive.float() {
527 with_array.push(PrimitiveOrComponent::Float(f));
528 } else if let Some(d) = primitive.double() {
529 with_array.push(PrimitiveOrComponent::Double(d));
530 } else if let Some(s) = primitive.string() {
531 with_array.push(PrimitiveOrComponent::String(s.to_string()));
532 } else {
533 warn!(
534 "couldn't parse {item:?} as FormattedText because it has a disallowed primitive"
535 );
536 with_array.push(PrimitiveOrComponent::String("?".to_owned()));
537 }
538 } else if let Some(c) = FormattedText::from_nbt_compound(item) {
539 if let FormattedText::Text(text_component) = c
540 && text_component.base.siblings.is_empty()
541 && text_component.base.style.is_empty()
542 {
543 with_array.push(PrimitiveOrComponent::String(text_component.text));
544 continue;
545 }
546 with_array.push(PrimitiveOrComponent::FormattedText(
547 FormattedText::from_nbt_compound(item)?,
548 ));
549 } else {
550 warn!("couldn't parse {item:?} as FormattedText");
551 with_array.push(PrimitiveOrComponent::String("?".to_owned()));
552 }
553 }
554 } else {
555 warn!(
556 "couldn't parse {with:?} as FormattedText because it's not a list of compounds"
557 );
558 return None;
559 }
560 component =
561 FormattedText::Translatable(TranslatableComponent::new(translate, with_array));
562 } else {
563 component =
565 FormattedText::Translatable(TranslatableComponent::new(translate, Vec::new()));
566 }
567 } else if let Some(score) = compound.compound("score") {
568 if score.get("name").is_none() || score.get("objective").is_none() {
569 trace!("A score component needs at least a name and an objective");
570 return None;
571 }
572 return None;
574 } else if compound.get("selector").is_some() {
575 trace!("selector text components aren't supported");
576 return None;
577 } else if compound.get("keybind").is_some() {
578 trace!("keybind text components aren't supported");
579 return None;
580 } else if compound.get("object").is_some() {
581 trace!("object text components aren't supported");
582 return None;
583 } else if let Some(tag) = compound.get("") {
584 return FormattedText::from_nbt_tag(tag);
585 } else {
586 let _nbt = compound.get("nbt")?;
587 let _separator = FormattedText::parse_separator_nbt(&compound)?;
588
589 let _interpret = match compound.get("interpret") {
590 Some(v) => v.byte().unwrap_or_default() != 0,
591 None => false,
592 };
593 if let Some(_block) = compound.get("block") {}
594 trace!("nbt text components aren't yet supported");
595 return None;
596 }
597 if let Some(extra) = compound.get("extra") {
598 if let Some(items) = extra.list() {
600 if let Some(items) = items.compounds() {
601 for item in items {
602 component.append(FormattedText::from_nbt_compound(item)?);
603 }
604 } else if let Some(items) = items.strings() {
605 for item in items {
606 component.append(FormattedText::from_nbt_string(item));
607 }
608 } else if let Some(items) = items.lists() {
609 for item in items {
610 component.append(FormattedText::from_nbt_list(item)?);
611 }
612 } else {
613 warn!(
614 "couldn't parse {items:?} as FormattedText because it's not a list of compounds or strings"
615 );
616 }
617 } else {
618 component.append(FormattedText::from_nbt_tag(extra)?);
619 }
620 }
621
622 let base_style = Style::from_compound(compound).ok()?;
623 let new_style = &mut component.get_base_mut().style;
624 **new_style = new_style.merged_with(&base_style);
625
626 Some(component)
627 }
628}
629
630#[cfg(feature = "simdnbt")]
631impl From<&simdnbt::Mutf8Str> for FormattedText {
632 fn from(s: &simdnbt::Mutf8Str) -> Self {
633 FormattedText::Text(TextComponent::new(s.to_string()))
634 }
635}
636
637#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
638impl AzBuf for FormattedText {
639 fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
640 use simdnbt::FromNbtTag;
641 use tracing::trace;
642
643 let nbt = simdnbt::borrow::read_optional_tag(buf)?;
644 trace!(
645 "Reading NBT for FormattedText: {:?}",
646 nbt.as_ref().map(|n| n.as_tag().to_owned())
647 );
648 match nbt {
649 Some(nbt) => FormattedText::from_nbt_tag(nbt.as_tag()).ok_or(BufReadError::Custom(
650 "couldn't convert nbt to chat message".to_owned(),
651 )),
652 _ => Ok(FormattedText::default()),
653 }
654 }
655 fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
656 use simdnbt::Serialize;
657
658 let mut out = Vec::new();
659 simdnbt::owned::BaseNbt::write_unnamed(&(self.clone().to_compound().into()), &mut out);
660 buf.write_all(&out)
661 }
662}
663
664impl From<String> for FormattedText {
665 fn from(s: String) -> Self {
666 FormattedText::Text(TextComponent {
667 text: s,
668 base: BaseComponent::default(),
669 })
670 }
671}
672impl From<&str> for FormattedText {
673 fn from(s: &str) -> Self {
674 Self::from(s.to_owned())
675 }
676}
677impl From<TranslatableComponent> for FormattedText {
678 fn from(c: TranslatableComponent) -> Self {
679 FormattedText::Translatable(c)
680 }
681}
682impl From<TextComponent> for FormattedText {
683 fn from(c: TextComponent) -> Self {
684 FormattedText::Text(c)
685 }
686}
687
688impl Display for FormattedText {
689 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
694 match self {
695 FormattedText::Text(c) => c.fmt(f),
696 FormattedText::Translatable(c) => c.fmt(f),
697 }
698 }
699}
700
701impl Default for FormattedText {
702 fn default() -> Self {
703 FormattedText::Text(TextComponent::default())
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use serde_json::Value;
710
711 use super::*;
712 use crate::style::TextColor;
713
714 #[test]
715 fn deserialize_translation() {
716 let j: Value =
717 serde_json::from_str(r#"{"translate": "translation.test.args", "with": ["a", "b"]}"#)
718 .unwrap();
719 let component = FormattedText::deserialize(&j).unwrap();
720 assert_eq!(
721 component,
722 FormattedText::Translatable(TranslatableComponent::new(
723 "translation.test.args".to_owned(),
724 vec![
725 PrimitiveOrComponent::String("a".to_owned()),
726 PrimitiveOrComponent::String("b".to_owned())
727 ]
728 ))
729 );
730 }
731
732 #[test]
733 fn deserialize_translation_invalid_arguments() {
734 let j: Value =
735 serde_json::from_str(r#"{"translate": "translation.test.args", "with": {}}"#).unwrap();
736 assert!(FormattedText::deserialize(&j).is_err());
737 }
738
739 #[test]
740 fn deserialize_translation_fallback() {
741 let j: Value = serde_json::from_str(r#"{"translate": "translation.test.undefined", "fallback": "fallback: %s", "with": ["a"]}"#).unwrap();
742 let component = FormattedText::deserialize(&j).unwrap();
743 assert_eq!(
744 component,
745 FormattedText::Translatable(TranslatableComponent::with_fallback(
746 "translation.test.undefined".to_owned(),
747 Some("fallback: %s".to_owned()),
748 vec![PrimitiveOrComponent::String("a".to_owned())]
749 ))
750 );
751 }
752
753 #[test]
754 fn deserialize_translation_invalid_fallback() {
755 let j: Value = serde_json::from_str(
756 r#"{"translate": "translation.test.undefined", "fallback": {"text": "invalid"}}"#,
757 )
758 .unwrap();
759 assert!(FormattedText::deserialize(&j).is_err());
760 }
761 #[test]
762 fn deserialize_translation_primitive_args() {
763 let j: Value = serde_json::from_str(
764 r#"{"translate":"commands.list.players", "with": [1, 65536, "<players>", {"text": "unused", "color": "red"}]}"#,
765 )
766 .unwrap();
767 assert_eq!(
768 FormattedText::deserialize(&j).unwrap(),
769 FormattedText::Translatable(TranslatableComponent::new(
770 "commands.list.players".to_owned(),
771 vec![
772 PrimitiveOrComponent::Short(1),
773 PrimitiveOrComponent::Integer(65536),
774 PrimitiveOrComponent::String("<players>".to_owned()),
775 PrimitiveOrComponent::FormattedText(FormattedText::Text(
776 TextComponent::new("unused")
777 .with_style(Style::new().color(Some(TextColor::parse("red").unwrap())))
778 ))
779 ]
780 ))
781 );
782 }
783
784 #[test]
785 fn test_translatable_with_color_inheritance() {
786 let json = serde_json::json!({
787 "translate": "advancements.story.smelt_iron.title",
788 "color": "green",
789 "with": [{"text": "Acquire Hardware"}]
790 });
791 let component = FormattedText::deserialize(&json).unwrap();
792 let ansi = component.to_ansi();
793 assert!(ansi.contains("\u{1b}[38;2;85;255;85m"));
794 }
795}