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>(
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_array = with
310 .as_array()
311 .ok_or_else(|| de::Error::custom("\"with\" must be an array"))?
312 .iter()
313 .map(|item| {
314 PrimitiveOrComponent::deserialize(item).map_err(de::Error::custom)
315 })
316 .collect::<Result<Vec<PrimitiveOrComponent>, _>>()?;
317
318 component = FormattedText::Translatable(TranslatableComponent::with_fallback(
319 translate, fallback, with_array,
320 ));
321 } else {
322 component = FormattedText::Translatable(TranslatableComponent::with_fallback(
324 translate,
325 fallback,
326 Vec::new(),
327 ));
328 }
329 } else if let Some(score) = json.get("score") {
330 if score.get("name").is_none() || score.get("objective").is_none() {
332 return Err(de::Error::missing_field(
333 "A score component needs at least a name and an objective",
334 ));
335 }
336 return Err(de::Error::custom(
338 "score text components aren't yet supported",
339 ));
340 } else if json.get("selector").is_some() {
341 return Err(de::Error::custom(
342 "selector text components aren't yet supported",
343 ));
344 } else if json.get("keybind").is_some() {
345 return Err(de::Error::custom(
346 "keybind text components aren't yet supported",
347 ));
348 } else if json.get("object").is_some() {
349 return Err(de::Error::custom(
350 "object text components aren't yet supported",
351 ));
352 } else {
353 let Some(_nbt) = json.get("nbt") else {
354 return Err(de::Error::custom(
355 format!("Don't know how to turn {json} into a FormattedText").as_str(),
356 ));
357 };
358 let _separator =
359 FormattedText::parse_separator(&json).map_err(de::Error::custom)?;
360
361 let _interpret = match json.get("interpret") {
362 Some(v) => v.as_bool().ok_or(Some(false)).unwrap(),
363 None => false,
364 };
365 if let Some(_block) = json.get("block") {}
366 return Err(de::Error::custom(
367 "nbt text components aren't yet supported",
368 ));
369 }
370 if let Some(extra) = json.get("extra") {
371 let Some(extra) = extra.as_array() else {
372 return Err(de::Error::custom("Extra isn't an array"));
373 };
374 if extra.is_empty() {
375 return Err(de::Error::custom("Unexpected empty array of components"));
376 }
377 for extra_component in extra {
378 let sibling =
379 FormattedText::deserialize(extra_component).map_err(de::Error::custom)?;
380 component.append(sibling);
381 }
382 }
383
384 let style = Style::deserialize(&json);
385 component.get_base_mut().style = Box::new(style);
386
387 return Ok(component);
388 }
389 else if !json.is_array() {
391 return Err(de::Error::custom(
392 format!("Don't know how to turn {json} into a FormattedText").as_str(),
393 ));
394 }
395 let json_array = json.as_array().unwrap();
396 let mut component =
399 FormattedText::deserialize(&json_array[0]).map_err(de::Error::custom)?;
400 for i in 1..json_array.len() {
401 component.append(
402 FormattedText::deserialize(json_array.get(i).unwrap())
403 .map_err(de::Error::custom)?,
404 );
405 }
406 Ok(component)
407 }
408}
409
410#[cfg(feature = "simdnbt")]
411impl simdnbt::Serialize for FormattedText {
412 fn to_compound(self) -> simdnbt::owned::NbtCompound {
413 match self {
414 FormattedText::Text(c) => c.to_compound(),
415 FormattedText::Translatable(c) => c.to_compound(),
416 }
417 }
418}
419
420#[cfg(feature = "simdnbt")]
421impl simdnbt::FromNbtTag for FormattedText {
422 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
423 if let Some(string) = tag.string() {
425 Some(FormattedText::from_nbt_string(string))
426 }
427 else if let Some(compound) = tag.compound() {
430 FormattedText::from_nbt_compound(compound)
431 }
432 else if let Some(list) = tag.list() {
434 FormattedText::from_nbt_list(list)
435 } else {
436 Some(FormattedText::Text(TextComponent::new("".to_owned())))
437 }
438 }
439}
440
441#[cfg(feature = "simdnbt")]
442impl FormattedText {
443 fn from_nbt_string(s: &simdnbt::Mutf8Str) -> Self {
444 FormattedText::from(s)
445 }
446 fn from_nbt_list(list: simdnbt::borrow::NbtList) -> Option<FormattedText> {
447 let mut component;
448 if let Some(compounds) = list.compounds() {
449 component = FormattedText::from_nbt_compound(compounds.first()?)?;
450 for compound in compounds.into_iter().skip(1) {
451 component.append(FormattedText::from_nbt_compound(compound)?);
452 }
453 } else if let Some(strings) = list.strings() {
454 component = FormattedText::from(*(strings.first()?));
455 for &string in strings.iter().skip(1) {
456 component.append(FormattedText::from(string));
457 }
458 } else {
459 debug!("couldn't parse {list:?} as FormattedText");
460 return None;
461 }
462 Some(component)
463 }
464
465 pub fn from_nbt_compound(compound: simdnbt::borrow::NbtCompound) -> Option<Self> {
466 let mut component: FormattedText;
467
468 if let Some(text) = compound.get("text") {
469 let text = text.string().unwrap_or_default().to_string();
470 component = FormattedText::Text(TextComponent::new(text));
471 } else if let Some(translate) = compound.get("translate") {
472 let translate = translate.string()?.into();
473 if let Some(with) = compound.get("with") {
474 let mut with_array = Vec::new();
475 let with_list = with.list()?;
476 if with_list.empty() {
477 } else if let Some(with) = with_list.strings() {
478 for item in with {
479 with_array.push(PrimitiveOrComponent::String(item.to_string()));
480 }
481 } else if let Some(with) = with_list.ints() {
482 for item in with {
483 with_array.push(PrimitiveOrComponent::Integer(item));
484 }
485 } else if let Some(with) = with_list.compounds() {
486 for item in with {
487 if let Some(primitive) = item.get("") {
492 if let Some(b) = primitive.byte() {
495 with_array.push(PrimitiveOrComponent::Boolean(b != 0));
497 } else if let Some(s) = primitive.short() {
498 with_array.push(PrimitiveOrComponent::Short(s));
499 } else if let Some(i) = primitive.int() {
500 with_array.push(PrimitiveOrComponent::Integer(i));
501 } else if let Some(l) = primitive.long() {
502 with_array.push(PrimitiveOrComponent::Long(l));
503 } else if let Some(f) = primitive.float() {
504 with_array.push(PrimitiveOrComponent::Float(f));
505 } else if let Some(d) = primitive.double() {
506 with_array.push(PrimitiveOrComponent::Double(d));
507 } else if let Some(s) = primitive.string() {
508 with_array.push(PrimitiveOrComponent::String(s.to_string()));
509 } else {
510 warn!(
511 "couldn't parse {item:?} as FormattedText because it has a disallowed primitive"
512 );
513 with_array.push(PrimitiveOrComponent::String("?".to_string()));
514 }
515 } else if let Some(c) = FormattedText::from_nbt_compound(item) {
516 if let FormattedText::Text(text_component) = c
517 && text_component.base.siblings.is_empty()
518 && text_component.base.style.is_empty()
519 {
520 with_array.push(PrimitiveOrComponent::String(text_component.text));
521 continue;
522 }
523 with_array.push(PrimitiveOrComponent::FormattedText(
524 FormattedText::from_nbt_compound(item)?,
525 ));
526 } else {
527 warn!("couldn't parse {item:?} as FormattedText");
528 with_array.push(PrimitiveOrComponent::String("?".to_string()));
529 }
530 }
531 } else {
532 warn!(
533 "couldn't parse {with:?} as FormattedText because it's not a list of compounds"
534 );
535 return None;
536 }
537 component =
538 FormattedText::Translatable(TranslatableComponent::new(translate, with_array));
539 } else {
540 component =
542 FormattedText::Translatable(TranslatableComponent::new(translate, Vec::new()));
543 }
544 } else if let Some(score) = compound.compound("score") {
545 if score.get("name").is_none() || score.get("objective").is_none() {
546 trace!("A score component needs at least a name and an objective");
547 return None;
548 }
549 return None;
551 } else if compound.get("selector").is_some() {
552 trace!("selector text components aren't supported");
553 return None;
554 } else if compound.get("keybind").is_some() {
555 trace!("keybind text components aren't supported");
556 return None;
557 } else if compound.get("object").is_some() {
558 trace!("object text components aren't supported");
559 return None;
560 } else if let Some(tag) = compound.get("") {
561 return FormattedText::from_nbt_tag(tag);
562 } else {
563 let _nbt = compound.get("nbt")?;
564 let _separator = FormattedText::parse_separator_nbt(&compound)?;
565
566 let _interpret = match compound.get("interpret") {
567 Some(v) => v.byte().unwrap_or_default() != 0,
568 None => false,
569 };
570 if let Some(_block) = compound.get("block") {}
571 trace!("nbt text components aren't yet supported");
572 return None;
573 }
574 if let Some(extra) = compound.get("extra") {
575 if let Some(items) = extra.list() {
577 if let Some(items) = items.compounds() {
578 for item in items {
579 component.append(FormattedText::from_nbt_compound(item)?);
580 }
581 } else if let Some(items) = items.strings() {
582 for item in items {
583 component.append(FormattedText::from_nbt_string(item));
584 }
585 } else if let Some(items) = items.lists() {
586 for item in items {
587 component.append(FormattedText::from_nbt_list(item)?);
588 }
589 } else {
590 warn!(
591 "couldn't parse {items:?} as FormattedText because it's not a list of compounds or strings"
592 );
593 }
594 } else {
595 component.append(FormattedText::from_nbt_tag(extra)?);
596 }
597 }
598
599 let base_style = Style::from_compound(compound).ok()?;
600 let new_style = &mut component.get_base_mut().style;
601 *new_style = Box::new(new_style.merged_with(&base_style));
602
603 Some(component)
604 }
605}
606
607#[cfg(feature = "simdnbt")]
608impl From<&simdnbt::Mutf8Str> for FormattedText {
609 fn from(s: &simdnbt::Mutf8Str) -> Self {
610 FormattedText::Text(TextComponent::new(s.to_string()))
611 }
612}
613
614#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
615impl AzaleaRead for FormattedText {
616 fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
617 let nbt = simdnbt::borrow::read_optional_tag(buf)?;
618 trace!(
619 "Reading NBT for FormattedText: {:?}",
620 nbt.as_ref().map(|n| n.as_tag().to_owned())
621 );
622 match nbt {
623 Some(nbt) => FormattedText::from_nbt_tag(nbt.as_tag()).ok_or(BufReadError::Custom(
624 "couldn't convert nbt to chat message".to_owned(),
625 )),
626 _ => Ok(FormattedText::default()),
627 }
628 }
629}
630
631#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
632impl AzaleaWrite for FormattedText {
633 fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
634 let mut out = Vec::new();
635 simdnbt::owned::BaseNbt::write_unnamed(&(self.clone().to_compound().into()), &mut out);
636 buf.write_all(&out)
637 }
638}
639
640impl From<String> for FormattedText {
641 fn from(s: String) -> Self {
642 FormattedText::Text(TextComponent {
643 text: s,
644 base: BaseComponent::default(),
645 })
646 }
647}
648impl From<&str> for FormattedText {
649 fn from(s: &str) -> Self {
650 Self::from(s.to_string())
651 }
652}
653impl From<TranslatableComponent> for FormattedText {
654 fn from(c: TranslatableComponent) -> Self {
655 FormattedText::Translatable(c)
656 }
657}
658impl From<TextComponent> for FormattedText {
659 fn from(c: TextComponent) -> Self {
660 FormattedText::Text(c)
661 }
662}
663
664impl Display for FormattedText {
665 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670 match self {
671 FormattedText::Text(c) => c.fmt(f),
672 FormattedText::Translatable(c) => c.fmt(f),
673 }
674 }
675}
676
677impl Default for FormattedText {
678 fn default() -> Self {
679 FormattedText::Text(TextComponent::default())
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use serde_json::Value;
686
687 use super::*;
688 use crate::style::TextColor;
689
690 #[test]
691 fn deserialize_translation() {
692 let j: Value =
693 serde_json::from_str(r#"{"translate": "translation.test.args", "with": ["a", "b"]}"#)
694 .unwrap();
695 let component = FormattedText::deserialize(&j).unwrap();
696 assert_eq!(
697 component,
698 FormattedText::Translatable(TranslatableComponent::new(
699 "translation.test.args".to_string(),
700 vec![
701 PrimitiveOrComponent::String("a".to_string()),
702 PrimitiveOrComponent::String("b".to_string())
703 ]
704 ))
705 );
706 }
707
708 #[test]
709 fn deserialize_translation_invalid_arguments() {
710 let j: Value =
711 serde_json::from_str(r#"{"translate": "translation.test.args", "with": {}}"#).unwrap();
712 assert!(FormattedText::deserialize(&j).is_err());
713 }
714
715 #[test]
716 fn deserialize_translation_fallback() {
717 let j: Value = serde_json::from_str(r#"{"translate": "translation.test.undefined", "fallback": "fallback: %s", "with": ["a"]}"#).unwrap();
718 let component = FormattedText::deserialize(&j).unwrap();
719 assert_eq!(
720 component,
721 FormattedText::Translatable(TranslatableComponent::with_fallback(
722 "translation.test.undefined".to_string(),
723 Some("fallback: %s".to_string()),
724 vec![PrimitiveOrComponent::String("a".to_string())]
725 ))
726 );
727 }
728
729 #[test]
730 fn deserialize_translation_invalid_fallback() {
731 let j: Value = serde_json::from_str(
732 r#"{"translate": "translation.test.undefined", "fallback": {"text": "invalid"}}"#,
733 )
734 .unwrap();
735 assert!(FormattedText::deserialize(&j).is_err());
736 }
737 #[test]
738 fn deserialize_translation_primitive_args() {
739 let j: Value = serde_json::from_str(
740 r#"{"translate":"commands.list.players", "with": [1, 65536, "<players>", {"text": "unused", "color": "red"}]}"#,
741 )
742 .unwrap();
743 assert_eq!(
744 FormattedText::deserialize(&j).unwrap(),
745 FormattedText::Translatable(TranslatableComponent::new(
746 "commands.list.players".to_string(),
747 vec![
748 PrimitiveOrComponent::Short(1),
749 PrimitiveOrComponent::Integer(65536),
750 PrimitiveOrComponent::String("<players>".to_string()),
751 PrimitiveOrComponent::FormattedText(FormattedText::Text(
752 TextComponent::new("unused")
753 .with_style(Style::new().color(Some(TextColor::parse("red").unwrap())))
754 ))
755 ]
756 ))
757 );
758 }
759}