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::{StringOrComponent, TranslatableComponent},
21};
22
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)]
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, &Style) -> (String, String),
124 S: FnMut(&str) -> String,
125 C: FnMut(&Style) -> String,
126 {
127 let mut output = String::new();
128 let mut running_style = Style::default();
129
130 for component in self.clone().into_iter() {
131 let component_text = match &component {
132 Self::Text(c) => c.text.to_string(),
133 Self::Translatable(c) => match c.read() {
134 Ok(c) => c.to_string(),
135 Err(_) => c.key.to_string(),
136 },
137 };
138
139 let component_style = &component.get_base().style;
140
141 let formatted_style = style_formatter(&running_style, component_style, default_style);
142 let formatted_text = text_formatter(&component_text);
143
144 output.push_str(&formatted_style.0);
145 output.push_str(&formatted_text);
146 output.push_str(&formatted_style.1);
147
148 if component_style.reset {
150 running_style = default_style.clone();
151 } else {
152 running_style.apply(component_style);
153 }
154 }
155
156 output.push_str(&cleanup_formatter(&running_style));
157
158 output
159 }
160
161 pub fn to_ansi_with_custom_style(&self, default_style: &Style) -> String {
167 self.to_custom_format(
168 |running, new, default| (running.compare_ansi(new, default), "".to_owned()),
169 |text| text.to_string(),
170 |style| if !style.is_empty() { "\u{1b}[m" } else { "" }.to_string(),
171 default_style,
172 )
173 }
174
175 pub fn to_ansi(&self) -> String {
197 self.to_ansi_with_custom_style(&DEFAULT_STYLE)
198 }
199
200 pub fn to_html(&self) -> String {
201 self.to_custom_format(
202 |running, new, _| {
203 (
204 format!(
205 "<span style=\"{}\">",
206 running.merged_with(new).get_html_style()
207 ),
208 "</span>".to_owned(),
209 )
210 },
211 |text| {
212 text.replace("&", "&")
213 .replace("<", "<")
214 .replace(">", ">")
216 .replace("\n", "<br>")
217 },
218 |_| "".to_string(),
219 &DEFAULT_STYLE,
220 )
221 }
222}
223
224impl IntoIterator for FormattedText {
225 fn into_iter(self) -> Self::IntoIter {
227 let base = self.get_base();
228 let siblings = base.siblings.clone();
229 let mut v: Vec<FormattedText> = Vec::with_capacity(siblings.len() + 1);
230 v.push(self);
231 for sibling in siblings {
232 v.extend(sibling);
233 }
234
235 v.into_iter()
236 }
237
238 type Item = FormattedText;
239 type IntoIter = std::vec::IntoIter<Self::Item>;
240}
241
242impl<'de> Deserialize<'de> for FormattedText {
243 fn deserialize<D>(de: D) -> Result<Self, D::Error>
244 where
245 D: Deserializer<'de>,
246 {
247 let json: serde_json::Value = serde::Deserialize::deserialize(de)?;
248
249 let mut component: FormattedText;
251
252 if !json.is_array() && !json.is_object() {
254 return Ok(FormattedText::Text(TextComponent::new(
255 json.as_str().unwrap_or("").to_string(),
256 )));
257 }
258 else if json.is_object() {
260 if let Some(text) = json.get("text") {
261 let text = text.as_str().unwrap_or("").to_string();
262 component = FormattedText::Text(TextComponent::new(text));
263 } else if let Some(translate) = json.get("translate") {
264 let translate = translate
265 .as_str()
266 .ok_or_else(|| de::Error::custom("\"translate\" must be a string"))?
267 .into();
268 if let Some(with) = json.get("with") {
269 let with = with
270 .as_array()
271 .ok_or_else(|| de::Error::custom("\"with\" must be an array"))?;
272 let mut with_array = Vec::with_capacity(with.len());
273 for item in with {
274 let c = FormattedText::deserialize(item).map_err(de::Error::custom)?;
278 if let FormattedText::Text(text_component) = c
279 && text_component.base.siblings.is_empty()
280 && text_component.base.style.is_empty()
281 {
282 with_array.push(StringOrComponent::String(text_component.text));
283 continue;
284 }
285 with_array.push(StringOrComponent::FormattedText(
286 FormattedText::deserialize(item).map_err(de::Error::custom)?,
287 ));
288 }
289 component = FormattedText::Translatable(TranslatableComponent::new(
290 translate, with_array,
291 ));
292 } else {
293 component = FormattedText::Translatable(TranslatableComponent::new(
295 translate,
296 Vec::new(),
297 ));
298 }
299 } else if let Some(score) = json.get("score") {
300 if score.get("name").is_none() || score.get("objective").is_none() {
302 return Err(de::Error::missing_field(
303 "A score component needs at least a name and an objective",
304 ));
305 }
306 return Err(de::Error::custom(
308 "score text components aren't yet supported",
309 ));
310 } else if json.get("selector").is_some() {
311 return Err(de::Error::custom(
312 "selector text components aren't yet supported",
313 ));
314 } else if json.get("keybind").is_some() {
315 return Err(de::Error::custom(
316 "keybind text components aren't yet supported",
317 ));
318 } else {
319 let Some(_nbt) = json.get("nbt") else {
320 return Err(de::Error::custom(
321 format!("Don't know how to turn {json} into a FormattedText").as_str(),
322 ));
323 };
324 let _separator =
325 FormattedText::parse_separator(&json).map_err(de::Error::custom)?;
326
327 let _interpret = match json.get("interpret") {
328 Some(v) => v.as_bool().ok_or(Some(false)).unwrap(),
329 None => false,
330 };
331 if let Some(_block) = json.get("block") {}
332 return Err(de::Error::custom(
333 "nbt text components aren't yet supported",
334 ));
335 }
336 if let Some(extra) = json.get("extra") {
337 let Some(extra) = extra.as_array() else {
338 return Err(de::Error::custom("Extra isn't an array"));
339 };
340 if extra.is_empty() {
341 return Err(de::Error::custom("Unexpected empty array of components"));
342 }
343 for extra_component in extra {
344 let sibling =
345 FormattedText::deserialize(extra_component).map_err(de::Error::custom)?;
346 component.append(sibling);
347 }
348 }
349
350 let style = Style::deserialize(&json);
351 component.get_base_mut().style = style;
352
353 return Ok(component);
354 }
355 else if !json.is_array() {
357 return Err(de::Error::custom(
358 format!("Don't know how to turn {json} into a FormattedText").as_str(),
359 ));
360 }
361 let json_array = json.as_array().unwrap();
362 let mut component =
365 FormattedText::deserialize(&json_array[0]).map_err(de::Error::custom)?;
366 for i in 1..json_array.len() {
367 component.append(
368 FormattedText::deserialize(json_array.get(i).unwrap())
369 .map_err(de::Error::custom)?,
370 );
371 }
372 Ok(component)
373 }
374}
375
376#[cfg(feature = "simdnbt")]
377impl simdnbt::Serialize for FormattedText {
378 fn to_compound(self) -> simdnbt::owned::NbtCompound {
379 match self {
380 FormattedText::Text(c) => c.to_compound(),
381 FormattedText::Translatable(c) => c.to_compound(),
382 }
383 }
384}
385
386#[cfg(feature = "simdnbt")]
387impl simdnbt::FromNbtTag for FormattedText {
388 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
389 if let Some(string) = tag.string() {
391 Some(FormattedText::from(string))
392 }
393 else if let Some(compound) = tag.compound() {
396 FormattedText::from_nbt_compound(compound)
397 }
398 else if let Some(list) = tag.list() {
400 let mut component;
401 if let Some(compounds) = list.compounds() {
402 component = FormattedText::from_nbt_compound(compounds.first()?)?;
403 for compound in compounds.into_iter().skip(1) {
404 component.append(FormattedText::from_nbt_compound(compound)?);
405 }
406 } else if let Some(strings) = list.strings() {
407 component = FormattedText::from(*(strings.first()?));
408 for &string in strings.iter().skip(1) {
409 component.append(FormattedText::from(string));
410 }
411 } else {
412 debug!("couldn't parse {list:?} as FormattedText");
413 return None;
414 }
415 Some(component)
416 } else {
417 Some(FormattedText::Text(TextComponent::new("".to_owned())))
418 }
419 }
420}
421
422#[cfg(feature = "simdnbt")]
423impl FormattedText {
424 pub fn from_nbt_compound(compound: simdnbt::borrow::NbtCompound) -> Option<Self> {
425 let mut component: FormattedText;
426
427 if let Some(text) = compound.get("text") {
428 let text = text.string().unwrap_or_default().to_string();
429 component = FormattedText::Text(TextComponent::new(text));
430 } else if let Some(translate) = compound.get("translate") {
431 let translate = translate.string()?.into();
432 if let Some(with) = compound.get("with") {
433 let mut with_array = Vec::new();
434 let with_list = with.list()?;
435 if with_list.empty() {
436 } else if let Some(with) = with_list.strings() {
437 for item in with {
438 with_array.push(StringOrComponent::String(item.to_string()));
439 }
440 } else if let Some(with) = with_list.ints() {
441 for item in with {
442 with_array.push(StringOrComponent::String(item.to_string()));
443 }
444 } else if let Some(with) = with_list.compounds() {
445 for item in with {
446 if let Some(primitive) = item.get("") {
451 if let Some(b) = primitive.byte() {
454 with_array.push(StringOrComponent::String(
456 if b != 0 { "true" } else { "false" }.to_string(),
457 ));
458 } else if let Some(s) = primitive.short() {
459 with_array.push(StringOrComponent::String(s.to_string()));
460 } else if let Some(i) = primitive.int() {
461 with_array.push(StringOrComponent::String(i.to_string()));
462 } else if let Some(l) = primitive.long() {
463 with_array.push(StringOrComponent::String(l.to_string()));
464 } else if let Some(f) = primitive.float() {
465 with_array.push(StringOrComponent::String(f.to_string()));
466 } else if let Some(d) = primitive.double() {
467 with_array.push(StringOrComponent::String(d.to_string()));
468 } else if let Some(s) = primitive.string() {
469 with_array.push(StringOrComponent::String(s.to_string()));
470 } else {
471 warn!(
472 "couldn't parse {item:?} as FormattedText because it has a disallowed primitive"
473 );
474 with_array.push(StringOrComponent::String("?".to_string()));
475 }
476 } else if let Some(c) = FormattedText::from_nbt_compound(item) {
477 if let FormattedText::Text(text_component) = c
478 && text_component.base.siblings.is_empty()
479 && text_component.base.style.is_empty()
480 {
481 with_array.push(StringOrComponent::String(text_component.text));
482 continue;
483 }
484 with_array.push(StringOrComponent::FormattedText(
485 FormattedText::from_nbt_compound(item)?,
486 ));
487 } else {
488 warn!("couldn't parse {item:?} as FormattedText");
489 with_array.push(StringOrComponent::String("?".to_string()));
490 }
491 }
492 } else {
493 warn!(
494 "couldn't parse {with:?} as FormattedText because it's not a list of compounds"
495 );
496 return None;
497 }
498 component =
499 FormattedText::Translatable(TranslatableComponent::new(translate, with_array));
500 } else {
501 component =
503 FormattedText::Translatable(TranslatableComponent::new(translate, Vec::new()));
504 }
505 } else if let Some(score) = compound.compound("score") {
506 if score.get("name").is_none() || score.get("objective").is_none() {
508 trace!("A score component needs at least a name and an objective");
510 return None;
511 }
512 return None;
514 } else if compound.get("selector").is_some() {
515 trace!("selector text components aren't yet supported");
517 return None;
518 } else if compound.get("keybind").is_some() {
519 trace!("keybind text components aren't yet supported");
521 return None;
522 } else if let Some(tag) = compound.get("") {
523 return FormattedText::from_nbt_tag(tag);
524 } else {
525 let _nbt = compound.get("nbt")?;
526 let _separator = FormattedText::parse_separator_nbt(&compound)?;
527
528 let _interpret = match compound.get("interpret") {
529 Some(v) => v.byte().unwrap_or_default() != 0,
530 None => false,
531 };
532 if let Some(_block) = compound.get("block") {}
533 trace!("nbt text components aren't yet supported");
534 return None;
535 }
536 if let Some(extra) = compound.get("extra") {
537 component.append(FormattedText::from_nbt_tag(extra)?);
538 }
539
540 let base_style = Style::from_compound(compound).ok()?;
541 let new_style = &mut component.get_base_mut().style;
542 *new_style = new_style.merged_with(&base_style);
543
544 Some(component)
545 }
546}
547
548#[cfg(feature = "simdnbt")]
549impl From<&simdnbt::Mutf8Str> for FormattedText {
550 fn from(s: &simdnbt::Mutf8Str) -> Self {
551 FormattedText::Text(TextComponent::new(s.to_string()))
552 }
553}
554
555#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
556impl AzaleaRead for FormattedText {
557 fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
558 let nbt = simdnbt::borrow::read_optional_tag(buf)?;
559 match nbt {
560 Some(nbt) => FormattedText::from_nbt_tag(nbt.as_tag()).ok_or(BufReadError::Custom(
561 "couldn't convert nbt to chat message".to_owned(),
562 )),
563 _ => Ok(FormattedText::default()),
564 }
565 }
566}
567
568#[cfg(all(feature = "azalea-buf", feature = "simdnbt"))]
569impl AzaleaWrite for FormattedText {
570 fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
571 let mut out = Vec::new();
572 simdnbt::owned::BaseNbt::write_unnamed(&(self.clone().to_compound().into()), &mut out);
573 buf.write_all(&out)
574 }
575}
576
577impl From<String> for FormattedText {
578 fn from(s: String) -> Self {
579 FormattedText::Text(TextComponent {
580 text: s,
581 base: BaseComponent::default(),
582 })
583 }
584}
585impl From<&str> for FormattedText {
586 fn from(s: &str) -> Self {
587 Self::from(s.to_string())
588 }
589}
590
591impl Display for FormattedText {
592 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593 match self {
594 FormattedText::Text(c) => c.fmt(f),
595 FormattedText::Translatable(c) => c.fmt(f),
596 }
597 }
598}
599
600impl Default for FormattedText {
601 fn default() -> Self {
602 FormattedText::Text(TextComponent::default())
603 }
604}